Jekyll2022-08-02T17:51:25+00:00https://hibbard.eu/feed.xmlJames HibbardWeb developer, network admin and authorJames HibbardHow to Create a CRUD App with Rails and React2022-04-01T00:00:00+00:002022-04-01T00:00:00+00:00https://hibbard.eu/crud-app-rails-react<p>Most web applications need to persist data in one form or other. When working with a server-side language, this is normally a straightforward task. However when you add a front-end JavaScript framework to the mix, things start to get a bit trickier.</p>
<p>In this tutorial I am going to demonstrate how to build a JSON API using Ruby on Rails and then code a fully-functional React frontend to interact with the API. The app we’ll be building is an event manager, which will let you create and manage a list of academic events.</p>
<p>The app will showcase basic CRUD functionality and will add a couple of extra features, such as a datepicker and search.</p>
<!--more-->
<p>This is what the finished product will look like.</p>
<p><img src="https://res.cloudinary.com/hibbard/image/upload/v1648453623/event-manager-hooks/01-the-finished-app.png" alt="Event Manager - Flash message" />
You can find the <a href="https://github.com/jameshibbard/react-rails-crud-app">complete code for the tutorial on GitHub</a>.</p>
<p>This post is also available in <a href="https://techracho.bpsinc.jp/hachi8833/2022_05_26/118202">Japanese</a>.</p>
<h2 id="prerequisites">Prerequisites</h2>
<p>To follow along, you’ll need both Ruby and Node installed on your system. For Ruby you can either go <a href="https://www.ruby-lang.org/en/downloads/">here</a> and download the official binaries for your system, or use a version manager such as <a href="https://github.com/rbenv/rbenv">rbenv</a>.</p>
<p>The same goes for Node. You can either go <a href="https://nodejs.org/en/">here</a> and download the official binaries for your system, or use a version manager such as <a href="https://github.com/creationix/nvm">nvm</a>.</p>
<p>In both cases I would encourage people to use a version manager. They are easy to set up and make managing multiple versions of Node/Ruby a breeze. They also help negate permissions problems, meaning you don’t end up having to install gems/packages with admin rights.</p>
<p>For this tutorial I’ll be using Ruby version 3.1 and Node version 16 (the latest LTS). My operating system is Linux Mint, so any terminal related commands will be tailored towards ‘nix.</p>
<h2 id="tech-stack">Tech Stack</h2>
<p>When building an app like this, there are many ways to accomplish the same goal. This section gives you an overview of the libraries I have used and the tech choices I have made.</p>
<p>I am using the following libraries:</p>
<ul>
<li><a href="https://rubyonrails.org/">Rails</a> version 7</li>
<li><a href="https://reactjs.org/">React</a> version 18</li>
<li><a href="https://reactrouterdotcom.fly.dev/docs/en/v6">React Router</a> version 6</li>
<li><a href="https://github.com/Pikaday/Pikaday">Pikaday</a></li>
<li><a href="https://fkhadra.github.io/react-toastify/introduction/">React-Toastify</a></li>
<li><a href="https://www.npmjs.com/package/prop-types">React Prop Types</a></li>
<li><a href="https://eslint.org/">ESLint</a></li>
</ul>
<p>I have used <a href="https://www.sqlite.org/index.html">SQLite</a> as a database, as this requires least setup and is what Rails uses as a default for a new app.</p>
<p>I am using <a href="https://esbuild.github.io/">esbuild</a> to bundle the React app, although I will also show how to set up <a href="https://github.com/shakacode/shakapacker">Shakapacker</a>, the successor to Webpacker.</p>
<p>To reflect current trends in the React community, this tutorial will use <a href="https://reactjs.org/docs/hooks-intro.html">hooks</a> and function components, <em>not</em> class-based components. If you would like to read an earlier version of this tutorial which uses class-based components, you can find that <a href="https://hibbard.eu/rails-react-crud-app-classes/">here</a>.</p>
<p>Otherwise, I have tried to keep packages and dependencies to a minimum. For example, I have used npm as a package manager (as opposed to installing Yarn) and am making any Ajax requests using the <a href="https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API">Fetch API</a>, as opposed to using a package such as Axios.</p>
<p>Finally, it is worth mentioning that the React app will live within the <code class="language-plaintext highlighter-rouge">app/javascript</code> folder of the Rails app. It would be possible to make the Rails app and the React app separate projects, but for an app of this size, I prefer to keep one within the other.</p>
<h2 id="choosing-a-bundler">Choosing a Bundler</h2>
<p>Before we issue the command to create a new Rails application, we have to decide how we are going to configure Rails to deal with our JavaScript.</p>
<p>There has been a lot happening in this space recently and one exciting new development is <a href="https://github.com/rails/importmap-rails">import maps</a>. This is the default in Rails 7.</p>
<p>As the name suggests, this feature lets you <em>import</em> JavaScript modules directly from a CDN (for example) and <em>map</em> these imports to versioned/digested files. This in turn enables you to build JavaScript applications without the need for a transpilation or bundling step.</p>
<p>Unfortunately, when building a React app, this approach isn’t perfect, as you will need to compile the JSX. This is where <a href="https://github.com/rails/jsbundling-rails/">JavaScript Bundling for Rails</a> comes in. This is a gem that allows you to use either esbuild, rollup.js, or webpack to bundle your JavaScript, then deliver it via the asset pipeline. Of these three bundling options, the Rails community seems most enthusiastic about <a href="https://esbuild.github.io/">esbuild</a>, so that is what I will be going with here.</p>
<p>I will however, also demonstrate how to set things up with <a href="https://github.com/shakacode/shakapacker">Shakapacker</a>. This is the successor to the now retired Webpacker gem. It provides a wrapper around the webpack build system, a standard webpack configuration and a reasonable set of defaults.</p>
<p>There are a couple of differences between the two solutions. esbuild is considerably more lightweight, but not as fully featured as Shakapacker. For example it offers no hot module replacement and code splitting is still a work in progress. Transforming ES6+ syntax to ES5 <a href="https://esbuild.github.io/content-types/#es5">is not supported in esbuild</a>, whereas with Shakapacker (which uses Babel), it is.</p>
<p>If you would like to read more about what JavaScript in Rails currently looks like, check out the following videos and blog post, all by by DHH (the creator of Rails):</p>
<ul>
<li><a href="https://world.hey.com/dhh/modern-web-apps-without-javascript-bundling-or-transpiling-a20f2755">Modern web apps without JavaScript bundling or transpiling</a></li>
<li><a href="https://www.youtube.com/watch?v=PtxZvFnL2i0">Alpha preview: Modern JavaScript in Rails 7 without Webpack</a></li>
<li><a href="https://www.youtube.com/watch?v=k73LKxim6tw">Alpha preview: Using React with importmaps on Rails 7</a></li>
<li><a href="https://www.youtube.com/watch?v=Chiu-0EVW3g">Alpha preview: Converting a import-mapped React app to use esbuild with JSX in Rails 7</a></li>
</ul>
<h2 id="creating-a-new-rails-app">Creating a New Rails App</h2>
<p>First, let’s install Rails and check the version number:</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>gem <span class="nb">install </span>rails
rails <span class="nt">-v</span>
<span class="o">=></span> 7.0.2.3
</code></pre></div></div>
<p>Then, choose a bundler (esbuild or Shakapacker) and follow the instructions below.</p>
<h3 id="esbuild">esbuild</h3>
<blockquote>
<p>Follow this section if you want a lightweight bundling solution, with no hot module replacement and no transformation of ES6+ syntax.</p>
</blockquote>
<p>Create a new Rails project like so:</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>rails new event-manager <span class="nt">-j</span> esbuild
</code></pre></div></div>
<p>Once the installer has run, change into the app directory and install React.</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">cd </span>event-manager
npm i react react-dom
</code></pre></div></div>
<p>If you open the <code class="language-plaintext highlighter-rouge">package.json</code> file in the project’s root, you’ll see that there is an npm script to build the app. Let’s add a second one to watch the <code class="language-plaintext highlighter-rouge">app/javascript</code> directory for changes and to rebundle everything when any are detected.</p>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nl">"scripts"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nl">"build"</span><span class="p">:</span><span class="w"> </span><span class="s2">"esbuild app/javascript/*.* --bundle --sourcemap --outdir=app/assets/builds --loader:.js=jsx"</span><span class="p">,</span><span class="w">
</span><span class="nl">"watch"</span><span class="p">:</span><span class="w"> </span><span class="s2">"esbuild app/javascript/*.* --watch --bundle --outdir=app/assets/builds --loader:.js=jsx"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>
<p>Notice that we have used <code class="language-plaintext highlighter-rouge">--loader:.js=jsx</code> to tell esbuild to allow JSX syntax in <code class="language-plaintext highlighter-rouge">.js</code> files. The alternative here is to give your JSX files a <code class="language-plaintext highlighter-rouge">.jsx</code> extension.</p>
<p>Finally edit the <code class="language-plaintext highlighter-rouge">Procfile.dev</code> file in the project root:</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">web</span><span class="pi">:</span> <span class="s">bin/rails server -p </span><span class="m">3000</span>
<span class="na">js</span><span class="pi">:</span> <span class="s">npm run watch</span>
</code></pre></div></div>
<p>Skip the next section and proceed to <a href="#creating-a-hello-world-react-app">Creating a Hello World React App</a>.</p>
<h3 id="shakapacker">Shakapacker</h3>
<blockquote>
<p>Follow this section if you want a fully featured bundler, with hot module replacement and transformation of ES6+ syntax via Babel.</p>
</blockquote>
<p>Create a new Rails project like so:</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>rails new event-manager <span class="nt">--skip-javascript</span>
</code></pre></div></div>
<p>Change into the newly created directory and add Shakapacker to your <code class="language-plaintext highlighter-rouge">Gemfile</code>:</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>bundle add shakapacker <span class="nt">--strict</span>
</code></pre></div></div>
<p>Like Webpacker, Shakapacker relies on yarn, so make sure you have that available:</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>npm i <span class="nt">-g</span> yarn
</code></pre></div></div>
<p>Then run the following commands:</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>./bin/bundle <span class="nb">install</span>
./bin/rails webpacker:install
yarn add react react-dom @babel/preset-react
</code></pre></div></div>
<blockquote>
<p><strong>Note:</strong> there is a <a href="https://github.com/shakacode/shakapacker/issues/123">bug in the latest version of Shakapacker</a> which results in it not being able to find the Webpacker configuration file when you run <code class="language-plaintext highlighter-rouge">rails webpacker:install</code>. If you encounter this, you can pin Shakapacker to its previous version like so: <code class="language-plaintext highlighter-rouge">bundle add shakapacker --version "6.2.1" --strict</code>.</p>
</blockquote>
<p>Update <code class="language-plaintext highlighter-rouge">package.json</code> to add the <code class="language-plaintext highlighter-rouge">@babel/preset-react</code>:</p>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nl">"babel"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nl">"presets"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
</span><span class="s2">"./node_modules/shakapacker/package/babel/preset.js"</span><span class="p">,</span><span class="w">
</span><span class="s2">"@babel/preset-react"</span><span class="w">
</span><span class="p">]</span><span class="w">
</span><span class="p">}</span><span class="err">,</span><span class="w">
</span></code></pre></div></div>
<h2 id="creating-a-hello-world-react-app">Creating a Hello World React App</h2>
<p>Generate a <code class="language-plaintext highlighter-rouge">site</code> controller with an <code class="language-plaintext highlighter-rouge">index</code> action. This is where the React app will be served from:</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>rails g controller site index
</code></pre></div></div>
<p>Replace the contents of <code class="language-plaintext highlighter-rouge">app/views/site/index.html.erb</code> with the following:</p>
<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt"><div</span> <span class="na">id=</span><span class="s">"root"</span><span class="nt">></div></span>
</code></pre></div></div>
<p>Next, create an <code class="language-plaintext highlighter-rouge">App</code> component for our React application inside of a <code class="language-plaintext highlighter-rouge">components</code> folder in the <code class="language-plaintext highlighter-rouge">app/javascript</code> directory:</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">mkdir </span>app/javascript/components
<span class="nb">touch </span>app/javascript/components/App.js
</code></pre></div></div>
<p>Add the following code to <code class="language-plaintext highlighter-rouge">app/javascript/application.js</code>:</p>
<div class="language-jsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="nx">React</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">react</span><span class="dl">'</span><span class="p">;</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">createRoot</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">react-dom/client</span><span class="dl">'</span><span class="p">;</span>
<span class="k">import</span> <span class="nx">HelloMessage</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">./components/App</span><span class="dl">'</span><span class="p">;</span>
<span class="kd">const</span> <span class="nx">container</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">getElementById</span><span class="p">(</span><span class="dl">'</span><span class="s1">root</span><span class="dl">'</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">root</span> <span class="o">=</span> <span class="nx">createRoot</span><span class="p">(</span><span class="nx">container</span><span class="p">);</span>
<span class="nb">document</span><span class="p">.</span><span class="nx">addEventListener</span><span class="p">(</span><span class="dl">'</span><span class="s1">DOMContentLoaded</span><span class="dl">'</span><span class="p">,</span> <span class="p">()</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">root</span><span class="p">.</span><span class="nx">render</span><span class="p">(<</span><span class="nc">HelloMessage</span> <span class="na">name</span><span class="p">=</span><span class="s">"World"</span> <span class="p">/>);</span>
<span class="p">});</span>
</code></pre></div></div>
<p>This imports a <code class="language-plaintext highlighter-rouge">HelloMessage</code> component and renders it in the <code class="language-plaintext highlighter-rouge">div</code> element we created above.</p>
<p>Add the following code to <code class="language-plaintext highlighter-rouge">app/javascript/components/App.js</code>:</p>
<div class="language-jsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="nx">React</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">react</span><span class="dl">'</span><span class="p">;</span>
<span class="kd">const</span> <span class="nx">HelloMessage</span> <span class="o">=</span> <span class="p">({</span> <span class="nx">name</span> <span class="p">})</span> <span class="o">=></span> <span class="p"><</span><span class="nt">h1</span><span class="p">></span>Hello, <span class="si">{</span><span class="nx">name</span><span class="si">}</span>!<span class="p"></</span><span class="nt">h1</span><span class="p">>;</span>
<span class="k">export</span> <span class="k">default</span> <span class="nx">HelloMessage</span><span class="p">;</span>
</code></pre></div></div>
<p>Finally, add a root route to the <code class="language-plaintext highlighter-rouge">config/routes.rb</code> file:</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">Rails</span><span class="p">.</span><span class="nf">application</span><span class="p">.</span><span class="nf">routes</span><span class="p">.</span><span class="nf">draw</span> <span class="k">do</span>
<span class="n">root</span> <span class="ss">to: </span><span class="s1">'site#index'</span>
<span class="k">end</span>
</code></pre></div></div>
<p>If you are using esbuild, run: <code class="language-plaintext highlighter-rouge">./bin/dev</code> from the project route.</p>
<p>If you are using Shakapacker, kick off the Rails server in one terminal and the webpack dev server in another:</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>rails s
./bin/webpacker-dev-server
</code></pre></div></div>
<p>Then hit <a href="http://localhost:3000/">http://localhost:3000/</a>. You should see our React app displaying a “Hello, World!” message. 🎉</p>
<h3 id="troubleshooting-sqlite">Troubleshooting SQLite</h3>
<p>Depending on your operating system, you may need to install some additional libraries for Rails to interface with SQLite correctly.</p>
<p>On macOS, it seems that <a href="https://flaviocopes.com/sqlite-how-to-install/">SQLite is preinstalled</a>, so no action is necessary.</p>
<p>On Linux you may need to install the SQLite 3 development files:</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>apt-get <span class="nb">install </span>libsqlite3-dev
</code></pre></div></div>
<p>On Windows you will likely need the SQLite binaries in your path, as suggested in <a href="https://stackoverflow.com/a/53553577/1136887">this Stack Overflow answer</a>.</p>
<p>If you run into any difficulties, try searching for the error message.</p>
<h3 id="hot-module-replacement">Hot Module Replacement</h3>
<p>As far as following along with this tutorial is concerned, the main difference between the two bundlers is what happens when you make changes to your project files.</p>
<p>In the case of esbuild, it will rebundle everything, but in order to see the changes you need to refresh the browser manually. It doesn’t seem like esbuild has any <a href="https://github.com/evanw/esbuild/issues/645#issuecomment-755215007">plans to support HMR</a>, but it does (kinda) work with live reloading. If you’re interested in that, check out <a href="https://dev.to/davidcolbyatx/live-reloading-with-ruby-on-rails-and-esbuild-4cdd">Live reloading with Ruby on Rails and esbuild</a>.</p>
<p>Shakapacker on the other hand, comes with live reloading (i.e. an automatic page refresh) out of the box. It can also do Hot Module Replacement (HMR) whereby it automatically updates only that part of the page that changed while preserving your app’s state. This is definitely very convenient, but comes at the cost of a whole bunch of dependencies.</p>
<p>Before deciding on which bundler to use, take a second to weigh up your choices and decide which approach is best for you and the app that you are building.</p>
<h3 id="enable-hmr-for-shakapacker">Enable HMR for Shakapacker</h3>
<p>To enable HMR for a React app when using Shakapacker, a little extra configuration is needed. If you are using esbuild, you can skip this section and go on to <a href="#building-the-api">Building the API</a>.</p>
<p>First off, hop into <code class="language-plaintext highlighter-rouge">config/webpacker.yml</code> set <code class="language-plaintext highlighter-rouge">hmr</code> is set to <code class="language-plaintext highlighter-rouge">true</code>.</p>
<p>Then alter <code class="language-plaintext highlighter-rouge">config/webpack/webpack.config.js</code> like so:</p>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="p">{</span> <span class="nx">webpackConfig</span><span class="p">,</span> <span class="nx">inliningCss</span> <span class="p">}</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="dl">'</span><span class="s1">shakapacker</span><span class="dl">'</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">ReactRefreshWebpackPlugin</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="dl">'</span><span class="s1">@pmmmwh/react-refresh-webpack-plugin</span><span class="dl">'</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">isDevelopment</span> <span class="o">=</span> <span class="nx">process</span><span class="p">.</span><span class="nx">env</span><span class="p">.</span><span class="nx">NODE_ENV</span> <span class="o">!==</span> <span class="dl">'</span><span class="s1">production</span><span class="dl">'</span><span class="p">;</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">isDevelopment</span> <span class="o">&&</span> <span class="nx">inliningCss</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">webpackConfig</span><span class="p">.</span><span class="nx">plugins</span><span class="p">.</span><span class="nx">push</span><span class="p">(</span>
<span class="k">new</span> <span class="nx">ReactRefreshWebpackPlugin</span><span class="p">({</span>
<span class="na">overlay</span><span class="p">:</span> <span class="p">{</span>
<span class="na">sockPort</span><span class="p">:</span> <span class="nx">webpackConfig</span><span class="p">.</span><span class="nx">devServer</span><span class="p">.</span><span class="nx">port</span><span class="p">,</span>
<span class="p">},</span>
<span class="p">})</span>
<span class="p">);</span>
<span class="p">}</span>
<span class="nx">module</span><span class="p">.</span><span class="nx">exports</span> <span class="o">=</span> <span class="nx">webpackConfig</span><span class="p">;</span>
</code></pre></div></div>
<p>Install the <a href="https://www.npmjs.com/package/react-refresh">react-refresh</a> package, as well as <a href="https://www.npmjs.com/package/@pmmmwh/react-refresh-webpack-plugin">@pmmmwh/react-refresh-webpack-plugin</a>:</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>yarn add <span class="nt">--dev</span> react-refresh @pmmmwh/react-refresh-webpack-plugin
</code></pre></div></div>
<p>Finally, delete the Babel configuration from <code class="language-plaintext highlighter-rouge">package.json</code>:</p>
<div class="language-diff highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gd">- "babel": {
- "presets": [
- "./node_modules/shakapacker/package/babel/preset.js",
- "@babel/preset-react"
- ]
- },
</span></code></pre></div></div>
<p>Then create a <code class="language-plaintext highlighter-rouge">babel.config.js</code> file in the root of project and add the following:</p>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">module</span><span class="p">.</span><span class="nx">exports</span> <span class="o">=</span> <span class="kd">function</span> <span class="p">(</span><span class="nx">api</span><span class="p">)</span> <span class="p">{</span>
<span class="kd">const</span> <span class="nx">defaultConfigFunc</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="dl">'</span><span class="s1">shakapacker/package/babel/preset.js</span><span class="dl">'</span><span class="p">)</span>
<span class="kd">const</span> <span class="nx">resultConfig</span> <span class="o">=</span> <span class="nx">defaultConfigFunc</span><span class="p">(</span><span class="nx">api</span><span class="p">)</span>
<span class="kd">const</span> <span class="nx">isDevelopmentEnv</span> <span class="o">=</span> <span class="nx">api</span><span class="p">.</span><span class="nx">env</span><span class="p">(</span><span class="dl">'</span><span class="s1">development</span><span class="dl">'</span><span class="p">)</span>
<span class="kd">const</span> <span class="nx">isProductionEnv</span> <span class="o">=</span> <span class="nx">api</span><span class="p">.</span><span class="nx">env</span><span class="p">(</span><span class="dl">'</span><span class="s1">production</span><span class="dl">'</span><span class="p">)</span>
<span class="kd">const</span> <span class="nx">isTestEnv</span> <span class="o">=</span> <span class="nx">api</span><span class="p">.</span><span class="nx">env</span><span class="p">(</span><span class="dl">'</span><span class="s1">test</span><span class="dl">'</span><span class="p">)</span>
<span class="kd">const</span> <span class="nx">changesOnDefault</span> <span class="o">=</span> <span class="p">{</span>
<span class="na">presets</span><span class="p">:</span> <span class="p">[</span>
<span class="p">[</span>
<span class="dl">'</span><span class="s1">@babel/preset-react</span><span class="dl">'</span><span class="p">,</span>
<span class="p">{</span>
<span class="na">development</span><span class="p">:</span> <span class="nx">isDevelopmentEnv</span> <span class="o">||</span> <span class="nx">isTestEnv</span><span class="p">,</span>
<span class="na">useBuiltIns</span><span class="p">:</span> <span class="kc">true</span>
<span class="p">}</span>
<span class="p">]</span>
<span class="p">].</span><span class="nx">filter</span><span class="p">(</span><span class="nb">Boolean</span><span class="p">),</span>
<span class="na">plugins</span><span class="p">:</span> <span class="p">[</span>
<span class="nx">isProductionEnv</span> <span class="o">&&</span> <span class="p">[</span><span class="dl">'</span><span class="s1">babel-plugin-transform-react-remove-prop-types</span><span class="dl">'</span><span class="p">,</span>
<span class="p">{</span>
<span class="na">removeImport</span><span class="p">:</span> <span class="kc">true</span>
<span class="p">}</span>
<span class="p">],</span>
<span class="nx">process</span><span class="p">.</span><span class="nx">env</span><span class="p">.</span><span class="nx">WEBPACK_SERVE</span> <span class="o">&&</span> <span class="dl">'</span><span class="s1">react-refresh/babel</span><span class="dl">'</span>
<span class="p">].</span><span class="nx">filter</span><span class="p">(</span><span class="nb">Boolean</span><span class="p">),</span>
<span class="p">}</span>
<span class="nx">resultConfig</span><span class="p">.</span><span class="nx">presets</span> <span class="o">=</span> <span class="p">[...</span><span class="nx">resultConfig</span><span class="p">.</span><span class="nx">presets</span><span class="p">,</span> <span class="p">...</span><span class="nx">changesOnDefault</span><span class="p">.</span><span class="nx">presets</span><span class="p">]</span>
<span class="nx">resultConfig</span><span class="p">.</span><span class="nx">plugins</span> <span class="o">=</span> <span class="p">[...</span><span class="nx">resultConfig</span><span class="p">.</span><span class="nx">plugins</span><span class="p">,</span> <span class="p">...</span><span class="nx">changesOnDefault</span><span class="p">.</span><span class="nx">plugins</span> <span class="p">]</span>
<span class="k">return</span> <span class="nx">resultConfig</span>
<span class="p">}</span>
</code></pre></div></div>
<p>Restart the server, refresh the browser, and with that, HMR for your React app is enabled. 🚀</p>
<h2 id="building-the-api">Building the API</h2>
<p>Let’s start off by generating an <code class="language-plaintext highlighter-rouge">Event</code> model:</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>rails g model Event <span class="se">\</span>
event_type:string <span class="se">\</span>
event_date:date <span class="se">\</span>
title:text <span class="se">\</span>
speaker:string <span class="se">\</span>
host:string <span class="se">\</span>
published:boolean
</code></pre></div></div>
<p>Migrate the database:</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>rake db:migrate
</code></pre></div></div>
<p>Next, seed the model with some test data. You can do this by creating a <code class="language-plaintext highlighter-rouge">db/seeds/events.json</code> file and adding the contents from the <a href="https://github.com/jameshibbard/react-rails-crud-app/blob/main/db/seeds/events.json">corresponding file in the project repo</a>.</p>
<p>Then in <code class="language-plaintext highlighter-rouge">db/seeds.rb</code>, add:</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">json</span> <span class="o">=</span> <span class="no">ActiveSupport</span><span class="o">::</span><span class="no">JSON</span><span class="p">.</span><span class="nf">decode</span><span class="p">(</span><span class="no">File</span><span class="p">.</span><span class="nf">read</span><span class="p">(</span><span class="s1">'db/seeds/events.json'</span><span class="p">))</span>
<span class="n">json</span><span class="p">.</span><span class="nf">each</span> <span class="k">do</span> <span class="o">|</span><span class="n">record</span><span class="o">|</span>
<span class="no">Event</span><span class="p">.</span><span class="nf">create!</span><span class="p">(</span><span class="n">record</span><span class="p">)</span>
<span class="k">end</span>
</code></pre></div></div>
<p>And run <code class="language-plaintext highlighter-rouge">rake db:seed</code>. Start up the rails console with <code class="language-plaintext highlighter-rouge">rails c</code> and confirm that you have some data:</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>rails c
Loading development environment <span class="o">(</span>Rails 7.0.2.3<span class="o">)</span>
irb<span class="o">(</span>main<span class="o">)</span>:001:0> Event.all.count
<span class="o">(</span>1.7ms<span class="o">)</span> SELECT sqlite_version<span class="o">(</span><span class="k">*</span><span class="o">)</span>
Event Count <span class="o">(</span>0.2ms<span class="o">)</span> SELECT COUNT<span class="o">(</span><span class="k">*</span><span class="o">)</span> FROM <span class="s2">"events"</span>
<span class="o">=></span> 6
</code></pre></div></div>
<h3 id="controllers">Controllers</h3>
<p>In the next step, we’ll create an <code class="language-plaintext highlighter-rouge">Events</code> controller to respond to incoming requests to our API. We’ll put the controller in its own folder, as we’re going to namespace it. This will keep our code nice and organized and allow us to create our own set of routes for the API.</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">mkdir </span>app/controllers/api
<span class="nb">touch </span>app/controllers/api/events_controller.rb
</code></pre></div></div>
<p>Add the following code to <code class="language-plaintext highlighter-rouge">app/controllers/api/events_controller.rb</code>:</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">Api::EventsController</span> <span class="o"><</span> <span class="no">ApplicationController</span>
<span class="n">before_action</span> <span class="ss">:set_event</span><span class="p">,</span> <span class="ss">only: </span><span class="sx">%i[show update destroy]</span>
<span class="k">def</span> <span class="nf">index</span>
<span class="vi">@events</span> <span class="o">=</span> <span class="no">Event</span><span class="p">.</span><span class="nf">all</span>
<span class="n">render</span> <span class="ss">json: </span><span class="vi">@events</span>
<span class="k">end</span>
<span class="k">def</span> <span class="nf">show</span>
<span class="n">render</span> <span class="ss">json: </span><span class="vi">@event</span>
<span class="k">end</span>
<span class="k">def</span> <span class="nf">create</span>
<span class="vi">@event</span> <span class="o">=</span> <span class="no">Event</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="n">event_params</span><span class="p">)</span>
<span class="k">if</span> <span class="vi">@event</span><span class="p">.</span><span class="nf">save</span>
<span class="n">render</span> <span class="ss">json: </span><span class="vi">@event</span><span class="p">,</span> <span class="ss">status: :created</span>
<span class="k">else</span>
<span class="n">render</span> <span class="ss">json: </span><span class="vi">@event</span><span class="p">.</span><span class="nf">errors</span><span class="p">,</span> <span class="ss">status: :unprocessable_entity</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">def</span> <span class="nf">update</span>
<span class="k">if</span> <span class="vi">@event</span><span class="p">.</span><span class="nf">update</span><span class="p">(</span><span class="n">event_params</span><span class="p">)</span>
<span class="n">render</span> <span class="ss">json: </span><span class="vi">@event</span><span class="p">,</span> <span class="ss">status: :ok</span>
<span class="k">else</span>
<span class="n">render</span> <span class="ss">json: </span><span class="vi">@event</span><span class="p">.</span><span class="nf">errors</span><span class="p">,</span> <span class="ss">status: :unprocessable_entity</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">def</span> <span class="nf">destroy</span>
<span class="vi">@event</span><span class="p">.</span><span class="nf">destroy</span>
<span class="k">end</span>
<span class="kp">private</span>
<span class="k">def</span> <span class="nf">set_event</span>
<span class="vi">@event</span> <span class="o">=</span> <span class="no">Event</span><span class="p">.</span><span class="nf">find</span><span class="p">(</span><span class="n">params</span><span class="p">[</span><span class="ss">:id</span><span class="p">])</span>
<span class="k">end</span>
<span class="k">def</span> <span class="nf">event_params</span>
<span class="n">params</span><span class="p">.</span><span class="nf">require</span><span class="p">(</span><span class="ss">:event</span><span class="p">).</span><span class="nf">permit</span><span class="p">(</span>
<span class="ss">:id</span><span class="p">,</span>
<span class="ss">:event_type</span><span class="p">,</span>
<span class="ss">:event_date</span><span class="p">,</span>
<span class="ss">:title</span><span class="p">,</span>
<span class="ss">:speaker</span><span class="p">,</span>
<span class="ss">:host</span><span class="p">,</span>
<span class="ss">:published</span><span class="p">,</span>
<span class="ss">:created_at</span><span class="p">,</span>
<span class="ss">:updated_at</span>
<span class="p">)</span>
<span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>
<p>This is a basic set of controller methods to make up the CRUD functionality of our API. Hopefully the code is easy enough to follow, as I don’t want to go into it in much depth here. If you are new to Rails and would like to find out more about API building, check out <a href="https://medium.com/geekculture/how-to-create-a-rails-backend-api-871fcddd6e20">How to Create a Rails Backend API</a>.</p>
<p>The final thing we need to do regarding controllers is to change the forgery protection method in <code class="language-plaintext highlighter-rouge">app/controllers/application_controller.rb</code>:</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">ApplicationController</span> <span class="o"><</span> <span class="no">ActionController</span><span class="o">::</span><span class="no">Base</span>
<span class="n">protect_from_forgery</span> <span class="ss">with: :null_session</span>
<span class="k">end</span>
</code></pre></div></div>
<p>The reason this is necessary is that Rails has a built in mechanism to protect against <a href="https://www.imperva.com/learn/application-security/csrf-cross-site-request-forgery/">cross site request forgery</a> (CSRF) attacks. By default this sees Rails generate a unique token and validate its authenticity with each POST PUT PATCH DELETE request. If the token is missing, Rails will throw an exception.</p>
<p>However, as we are building a single-page app, we will only have a fresh token upon first render, which means we will need to alter this behavior. The above code ensures that if no CSRF token is provided, Rails will respond with an empty session, which will prevent any other scripts from using our authenticated session to do bad things</p>
<p>If you’d like to read more about this, check out:</p>
<ul>
<li><a href="https://marcgg.com/blog/2016/08/22/csrf-rails/">Understanding Rails’ Forgery Protection Strategies</a></li>
<li><a href="https://medium.com/rubyinside/a-deep-dive-into-csrf-protection-in-rails-19fa0a42c0ef">A Deep Dive into CSRF Protection in Rails</a></li>
<li><a href="https://blog.eq8.eu/article/rails-api-authentication-with-spa-csrf-tokens.html">Rails CSRF protection for SPA</a></li>
<li><a href="https://thinkster.io/tutorials/rails-json-api/configuring-rails-as-a-json-api">Configuring Rails as a JSON API</a></li>
</ul>
<h3 id="routes">Routes</h3>
<p>Finally let’s fix up the routes in <code class="language-plaintext highlighter-rouge">config/routes.rb</code>. The routing for the controller has to consider the fact that it’s within the <code class="language-plaintext highlighter-rouge">Api</code> namespace. We’ll do this using the <code class="language-plaintext highlighter-rouge">namespace</code> method.</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">Rails</span><span class="p">.</span><span class="nf">application</span><span class="p">.</span><span class="nf">routes</span><span class="p">.</span><span class="nf">draw</span> <span class="k">do</span>
<span class="n">root</span> <span class="ss">to: </span><span class="s1">'site#index'</span>
<span class="n">namespace</span> <span class="ss">:api</span> <span class="k">do</span>
<span class="n">resources</span> <span class="ss">:events</span><span class="p">,</span> <span class="ss">only: </span><span class="sx">%i[index show create destroy update]</span>
<span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>
<p>At this point if you can hit the various endpoints ( e.g. <a href="http://localhost:3000/api/events">http://localhost:3000/api/events</a>) and interact with the API.</p>
<p>You might also like to test the API with <a href="https://www.postman.com/">Postman</a>. Here’s how you would create a new event.</p>
<p>Set the request type to <em>POST</em>, the URL to <code class="language-plaintext highlighter-rouge">http://localhost:3000/api/events</code>, the <em>Headers</em> to <code class="language-plaintext highlighter-rouge">Content-Type: application/json</code> and under <em>Body > raw</em> enter:</p>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
</span><span class="nl">"event"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nl">"event_type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Colloquium"</span><span class="p">,</span><span class="w">
</span><span class="nl">"event_date"</span><span class="p">:</span><span class="w"> </span><span class="s2">"2022-07-21"</span><span class="p">,</span><span class="w">
</span><span class="nl">"title"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Investigating the Battle of Hastings"</span><span class="p">,</span><span class="w">
</span><span class="nl">"speaker"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Sarah Croix"</span><span class="p">,</span><span class="w">
</span><span class="nl">"host"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Jim Bradbury"</span><span class="p">,</span><span class="w">
</span><span class="nl">"published"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>
<p>Then hit send and you should see a response similar to that below.</p>
<p><img src="https://res.cloudinary.com/hibbard/image/upload/v1648646327/event-manager-hooks/02-testing-api-with-postman.png" alt="Testing the Rails API with Postman" />
You could also accomplish the same thing using curl:</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>curl <span class="nt">--location</span> <span class="nt">--request</span> POST <span class="s1">'http://localhost:3000/api/events'</span> <span class="se">\</span>
<span class="nt">--header</span> <span class="s1">'Content-Type: application/json'</span> <span class="se">\</span>
<span class="nt">--data-raw</span> <span class="s1">'{
"event": {
"event_type": "Colloquium",
"event_date": "2022-07-21",
"title": "Investigating the Battle of Hastings",
"speaker": "Sarah Croix",
"host": "Jim Bradbury",
"published": false
}
}'</span>
</code></pre></div></div>
<p>Before moving on, check either the Rails console or <a href="http://localhost:3000/api/events">http://localhost:3000/api/events</a> to satisfy yourself that the event has been created.</p>
<h2 id="scaffolding-the-event-manager">Scaffolding the Event Manager</h2>
<p>Next we need to think about how to structure our app’s UI. We’ll start off with an <code class="language-plaintext highlighter-rouge"><Editor></code> component which will contain the following child components:</p>
<ul>
<li>A <code class="language-plaintext highlighter-rouge"><Header></code> component to display our app’s title</li>
<li>An <code class="language-plaintext highlighter-rouge"><EventList></code> component to display a list of events</li>
<li>An <code class="language-plaintext highlighter-rouge"><Event></code> component to display individual events</li>
<li>An <code class="language-plaintext highlighter-rouge"><EventForm></code> component to allow us to edit and create events</li>
</ul>
<p>The whole thing will look like this:</p>
<p><img src="https://res.cloudinary.com/hibbard/image/upload/v1548691454/event-manager/react-app-wireframe.png" alt="React App Wireframe" /></p>
<h2 id="fetching-events">Fetching Events</h2>
<p>Let’s start off by creating the files we will need in this section:</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">touch </span>app/javascript/components/<span class="o">{</span>Editor.js,Header.js,EventList.js<span class="o">}</span>
</code></pre></div></div>
<blockquote>
<p>Please note that from now on I won’t give the full path of the React components. They are all located in <code class="language-plaintext highlighter-rouge">app/javascript/components</code></p>
</blockquote>
<p>Next, install React’s <a href="https://www.npmjs.com/package/prop-types">prop-types package</a>. This package will allow us to to document the intended types of component properties and also to make sure any values passed are of the correct data type.</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>npm i prop-types
</code></pre></div></div>
<blockquote>
<p>If you are using Shakapacker, remember to use Yarn to install your dependencies. In this case, the command would be <code class="language-plaintext highlighter-rouge">yarn add prop-types</code>.</p>
</blockquote>
<p>Alter <code class="language-plaintext highlighter-rouge">app/javascript/application.js</code> thus:</p>
<div class="language-jsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="nx">React</span><span class="p">,</span> <span class="p">{</span> <span class="nx">StrictMode</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">react</span><span class="dl">'</span><span class="p">;</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">createRoot</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">react-dom/client</span><span class="dl">'</span><span class="p">;</span>
<span class="k">import</span> <span class="nx">App</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">./components/App</span><span class="dl">'</span><span class="p">;</span>
<span class="kd">const</span> <span class="nx">container</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">getElementById</span><span class="p">(</span><span class="dl">'</span><span class="s1">root</span><span class="dl">'</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">root</span> <span class="o">=</span> <span class="nx">createRoot</span><span class="p">(</span><span class="nx">container</span><span class="p">);</span>
<span class="nb">document</span><span class="p">.</span><span class="nx">addEventListener</span><span class="p">(</span><span class="dl">'</span><span class="s1">DOMContentLoaded</span><span class="dl">'</span><span class="p">,</span> <span class="p">()</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">root</span><span class="p">.</span><span class="nx">render</span><span class="p">(</span>
<span class="p"><</span><span class="nc">StrictMode</span><span class="p">></span>
<span class="p"><</span><span class="nc">App</span> <span class="p">/></span>
<span class="p"></</span><span class="nc">StrictMode</span><span class="p">></span>
<span class="p">);</span>
<span class="p">});</span>
</code></pre></div></div>
<blockquote>
<p>When using esbuild, Rails will add imports for “@hotwired/turbo-rails” and “./controllers”. These relate to <a href="https://github.com/hotwired/turbo-rails">Turbo</a> and <a href="https://github.com/hotwired/stimulus-rails">Stimulus</a>, which together form the core of <a href="https://hotwired.dev/">Hotwire</a>. They are not relevant to our React application and I would recommend just leaving them as they are.</p>
</blockquote>
<p>You will also notice that we are wrapping the <code class="language-plaintext highlighter-rouge"><App></code> component in a <code class="language-plaintext highlighter-rouge"><StrictMode></code> component. This does not render any visible UI, rather it is a helper component that activates additional checks and warnings for its descendants while in development mode. You can <a href="https://reactjs.org/docs/strict-mode.html">read more about it here</a>.</p>
<h3 id="data-fetching-with-react-hooks">Data Fetching With React Hooks</h3>
<p>Now we can get on to building the React app. Let’s start off in <code class="language-plaintext highlighter-rouge">App.js</code> where we will require and render our <code class="language-plaintext highlighter-rouge"><Editor></code> component.</p>
<div class="language-jsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="nx">React</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">react</span><span class="dl">'</span><span class="p">;</span>
<span class="k">import</span> <span class="nx">Editor</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">./Editor</span><span class="dl">'</span><span class="p">;</span>
<span class="kd">const</span> <span class="nx">App</span> <span class="o">=</span> <span class="p">()</span> <span class="o">=></span> <span class="p"><</span><span class="nc">Editor</span> <span class="p">/>;</span>
<span class="k">export</span> <span class="k">default</span> <span class="nx">App</span><span class="p">;</span>
</code></pre></div></div>
<p>Next, add the following code to <code class="language-plaintext highlighter-rouge">Editor.js</code>:</p>
<div class="language-jsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="nx">React</span><span class="p">,</span> <span class="p">{</span> <span class="nx">useState</span><span class="p">,</span> <span class="nx">useEffect</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">react</span><span class="dl">'</span><span class="p">;</span>
<span class="k">import</span> <span class="nx">Header</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">./Header</span><span class="dl">'</span><span class="p">;</span>
<span class="k">import</span> <span class="nx">EventList</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">./EventList</span><span class="dl">'</span><span class="p">;</span>
<span class="kd">const</span> <span class="nx">Editor</span> <span class="o">=</span> <span class="p">()</span> <span class="o">=></span> <span class="p">{</span>
<span class="kd">const</span> <span class="p">[</span><span class="nx">events</span><span class="p">,</span> <span class="nx">setEvents</span><span class="p">]</span> <span class="o">=</span> <span class="nx">useState</span><span class="p">([]);</span>
<span class="kd">const</span> <span class="p">[</span><span class="nx">isLoading</span><span class="p">,</span> <span class="nx">setIsLoading</span><span class="p">]</span> <span class="o">=</span> <span class="nx">useState</span><span class="p">(</span><span class="kc">true</span><span class="p">);</span>
<span class="kd">const</span> <span class="p">[</span><span class="nx">isError</span><span class="p">,</span> <span class="nx">setIsError</span><span class="p">]</span> <span class="o">=</span> <span class="nx">useState</span><span class="p">(</span><span class="kc">false</span><span class="p">);</span>
<span class="nx">useEffect</span><span class="p">(()</span> <span class="o">=></span> <span class="p">{</span>
<span class="kd">const</span> <span class="nx">fetchData</span> <span class="o">=</span> <span class="k">async</span> <span class="p">()</span> <span class="o">=></span> <span class="p">{</span>
<span class="k">try</span> <span class="p">{</span>
<span class="kd">const</span> <span class="nx">response</span> <span class="o">=</span> <span class="k">await</span> <span class="nb">window</span><span class="p">.</span><span class="nx">fetch</span><span class="p">(</span><span class="dl">'</span><span class="s1">/api/events</span><span class="dl">'</span><span class="p">);</span>
<span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">response</span><span class="p">.</span><span class="nx">ok</span><span class="p">)</span> <span class="k">throw</span> <span class="nb">Error</span><span class="p">(</span><span class="nx">response</span><span class="p">.</span><span class="nx">statusText</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">data</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">response</span><span class="p">.</span><span class="nx">json</span><span class="p">();</span>
<span class="nx">setEvents</span><span class="p">(</span><span class="nx">data</span><span class="p">);</span>
<span class="p">}</span> <span class="k">catch</span> <span class="p">(</span><span class="nx">error</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">setIsError</span><span class="p">(</span><span class="kc">true</span><span class="p">);</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">error</span><span class="p">(</span><span class="nx">error</span><span class="p">);</span>
<span class="p">}</span>
<span class="nx">setIsLoading</span><span class="p">(</span><span class="kc">false</span><span class="p">);</span>
<span class="p">};</span>
<span class="nx">fetchData</span><span class="p">();</span>
<span class="p">},</span> <span class="p">[]);</span>
<span class="k">return</span> <span class="p">(</span>
<span class="p"><></span>
<span class="p"><</span><span class="nc">Header</span> <span class="p">/></span>
<span class="si">{</span><span class="nx">isError</span> <span class="o">&&</span> <span class="p"><</span><span class="nt">p</span><span class="p">></span>Something went wrong. Check the console.<span class="p"></</span><span class="nt">p</span><span class="p">></span><span class="si">}</span>
<span class="si">{</span><span class="nx">isLoading</span> <span class="p">?</span> <span class="p"><</span><span class="nt">p</span><span class="p">></span>Loading...<span class="p"></</span><span class="nt">p</span><span class="p">></span> <span class="p">:</span> <span class="p"><</span><span class="nc">EventList</span> <span class="na">events</span><span class="p">=</span><span class="si">{</span><span class="nx">events</span><span class="si">}</span> <span class="p">/></span><span class="si">}</span>
<span class="p"></></span>
<span class="p">);</span>
<span class="p">};</span>
<span class="k">export</span> <span class="k">default</span> <span class="nx">Editor</span><span class="p">;</span>
</code></pre></div></div>
<p>There is a bit more going on here, so let’s break it down. Essentially we want to contact our API, grab a list of events and pass them to the <code class="language-plaintext highlighter-rouge"><EventList></code> component, so that it can display them on the page.</p>
<p>We start off by employing the <a href="https://reactjs.org/docs/hooks-reference.html#usestate">useState hook</a> to declare three variables in state (<code class="language-plaintext highlighter-rouge">events</code>, <code class="language-plaintext highlighter-rouge">isLoading</code> and <code class="language-plaintext highlighter-rouge">isError</code>), as well as functions to set the values of these variables. We also assign them some initial values.</p>
<p>Next comes a <a href="https://reactjs.org/docs/hooks-reference.html#useeffect">useEffect hook</a> to handle our data fetching. As we are passing it an empty array as a second argument, this will run once when the component is rendered. This functions similarly to <code class="language-plaintext highlighter-rouge">componentDidMount</code> in a class-based component.</p>
<p>Inside the <code class="language-plaintext highlighter-rouge">useEffect</code> hook, we declare a <code class="language-plaintext highlighter-rouge">fetchData</code> function, which uses the Fetch API to hit the <code class="language-plaintext highlighter-rouge">/api/events</code> endpoint. Assuming this returns a valid JSON response (a list of events), we save that to the <code class="language-plaintext highlighter-rouge">events</code> variable in state.</p>
<p>The data fetching happens within a <code class="language-plaintext highlighter-rouge">try... catch</code> block, so that we can handle any errors that might occur. Note that the Promise returned from <code class="language-plaintext highlighter-rouge">fetch()</code> won’t reject according to HTTP error status, even if the response is 404 or 500. This is why we have to inspect the response’s <a href="https://developer.mozilla.org/en-US/docs/Web/API/Response/ok">ok</a> property and catch any errors manually. You can read more about that here: <a href="https://www.tjvantoll.com/2015/09/13/fetch-and-errors/">Handling Failed HTTP Responses With fetch()</a>.</p>
<p>Once the data fetching has completed, we set the <code class="language-plaintext highlighter-rouge">isLoading</code> variable to <code class="language-plaintext highlighter-rouge">false</code>.</p>
<blockquote>
<p>If you want to check the loading effect, add a <code class="language-plaintext highlighter-rouge">sleep 5</code> to the <code class="language-plaintext highlighter-rouge">index</code> method in the <code class="language-plaintext highlighter-rouge">EventsController</code>.</p>
</blockquote>
<p>The last thing we do inside the hook is to invoke the <code class="language-plaintext highlighter-rouge">fetchData</code> function. We need a separate function here, as we cannot mark the callback function we pass to the <code class="language-plaintext highlighter-rouge">useEffect</code> hook as being <code class="language-plaintext highlighter-rouge">async</code>.</p>
<p>Finally, we return some JSX. This consists of the <code class="language-plaintext highlighter-rouge"><Header></code> component we will declare shortly, then either an error message, a loading message, or the <code class="language-plaintext highlighter-rouge"><EventList></code> component to which we pass a list of events. The <code class="language-plaintext highlighter-rouge"><Editor></code> component works out which of these to render based on the value of the <code class="language-plaintext highlighter-rouge">isError</code> and <code class="language-plaintext highlighter-rouge">isLoading</code> variables we declared previously.</p>
<blockquote>
<p>If you are new to using hooks in React, I suggest checking out <a href="https://www.sitepoint.com/react-hooks/">this article over on SitePoint</a> to get up to speed. If you would like to dive into data fetching in React using hooks, I recommend <a href="https://www.robinwieruch.de/react-hooks-fetch-data/">this tutorial by Robin Wieruch</a>.</p>
</blockquote>
<p>In <code class="language-plaintext highlighter-rouge">Header.js</code>:</p>
<div class="language-jsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="nx">React</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">react</span><span class="dl">'</span><span class="p">;</span>
<span class="kd">const</span> <span class="nx">Header</span> <span class="o">=</span> <span class="p">()</span> <span class="o">=></span> <span class="p">(</span>
<span class="p"><</span><span class="nt">header</span><span class="p">></span>
<span class="p"><</span><span class="nt">h1</span><span class="p">></span>Event Manager<span class="p"></</span><span class="nt">h1</span><span class="p">></span>
<span class="p"></</span><span class="nt">header</span><span class="p">></span>
<span class="p">);</span>
<span class="k">export</span> <span class="k">default</span> <span class="nx">Header</span><span class="p">;</span>
</code></pre></div></div>
<p>Nothing exciting going on here. We’re just rendering a header element.</p>
<p>In <code class="language-plaintext highlighter-rouge">EventList.js</code> add the following:</p>
<div class="language-jsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="nx">React</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">react</span><span class="dl">'</span><span class="p">;</span>
<span class="k">import</span> <span class="nx">PropTypes</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">prop-types</span><span class="dl">'</span><span class="p">;</span>
<span class="kd">const</span> <span class="nx">EventList</span> <span class="o">=</span> <span class="p">({</span> <span class="nx">events</span> <span class="p">})</span> <span class="o">=></span> <span class="p">{</span>
<span class="kd">const</span> <span class="nx">renderEvents</span> <span class="o">=</span> <span class="p">(</span><span class="nx">eventArray</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">eventArray</span><span class="p">.</span><span class="nx">sort</span><span class="p">((</span><span class="nx">a</span><span class="p">,</span> <span class="nx">b</span><span class="p">)</span> <span class="o">=></span> <span class="k">new</span> <span class="nb">Date</span><span class="p">(</span><span class="nx">b</span><span class="p">.</span><span class="nx">event_date</span><span class="p">)</span> <span class="o">-</span> <span class="k">new</span> <span class="nb">Date</span><span class="p">(</span><span class="nx">a</span><span class="p">.</span><span class="nx">event_date</span><span class="p">));</span>
<span class="k">return</span> <span class="nx">eventArray</span><span class="p">.</span><span class="nx">map</span><span class="p">((</span><span class="nx">event</span><span class="p">)</span> <span class="o">=></span> <span class="p">(</span>
<span class="p"><</span><span class="nt">li</span> <span class="na">key</span><span class="p">=</span><span class="si">{</span><span class="nx">event</span><span class="p">.</span><span class="nx">id</span><span class="si">}</span><span class="p">></span>
<span class="si">{</span><span class="nx">event</span><span class="p">.</span><span class="nx">event_date</span><span class="si">}</span>
<span class="si">{</span><span class="dl">'</span><span class="s1"> - </span><span class="dl">'</span><span class="si">}</span>
<span class="si">{</span><span class="nx">event</span><span class="p">.</span><span class="nx">event_type</span><span class="si">}</span>
<span class="p"></</span><span class="nt">li</span><span class="p">></span>
<span class="p">));</span>
<span class="p">};</span>
<span class="k">return</span> <span class="p">(</span>
<span class="p"><</span><span class="nt">section</span><span class="p">></span>
<span class="p"><</span><span class="nt">h2</span><span class="p">></span>Events<span class="p"></</span><span class="nt">h2</span><span class="p">></span>
<span class="p"><</span><span class="nt">ul</span><span class="p">></span><span class="si">{</span><span class="nx">renderEvents</span><span class="p">(</span><span class="nx">events</span><span class="p">)</span><span class="si">}</span><span class="p"></</span><span class="nt">ul</span><span class="p">></span>
<span class="p"></</span><span class="nt">section</span><span class="p">></span>
<span class="p">);</span>
<span class="p">};</span>
<span class="nx">EventList</span><span class="p">.</span><span class="nx">propTypes</span> <span class="o">=</span> <span class="p">{</span>
<span class="na">events</span><span class="p">:</span> <span class="nx">PropTypes</span><span class="p">.</span><span class="nx">arrayOf</span><span class="p">(</span><span class="nx">PropTypes</span><span class="p">.</span><span class="nx">shape</span><span class="p">({</span>
<span class="na">id</span><span class="p">:</span> <span class="nx">PropTypes</span><span class="p">.</span><span class="nx">number</span><span class="p">,</span>
<span class="na">event_type</span><span class="p">:</span> <span class="nx">PropTypes</span><span class="p">.</span><span class="nx">string</span><span class="p">,</span>
<span class="na">event_date</span><span class="p">:</span> <span class="nx">PropTypes</span><span class="p">.</span><span class="nx">string</span><span class="p">,</span>
<span class="na">title</span><span class="p">:</span> <span class="nx">PropTypes</span><span class="p">.</span><span class="nx">string</span><span class="p">,</span>
<span class="na">speaker</span><span class="p">:</span> <span class="nx">PropTypes</span><span class="p">.</span><span class="nx">string</span><span class="p">,</span>
<span class="na">host</span><span class="p">:</span> <span class="nx">PropTypes</span><span class="p">.</span><span class="nx">string</span><span class="p">,</span>
<span class="na">published</span><span class="p">:</span> <span class="nx">PropTypes</span><span class="p">.</span><span class="nx">bool</span><span class="p">,</span>
<span class="p">})).</span><span class="nx">isRequired</span><span class="p">,</span>
<span class="p">};</span>
<span class="k">export</span> <span class="k">default</span> <span class="nx">EventList</span><span class="p">;</span>
</code></pre></div></div>
<p>This component receives an array of event objects as props (<code class="language-plaintext highlighter-rouge">events</code>) and is responsible for displaying them as an ordered list. This happens in the <code class="language-plaintext highlighter-rouge">renderEvents</code> method which sorts the array by date in descending order, then renders a list item for each event.</p>
<p>Note that we have also implemented some prop validation to ensure that the <code class="language-plaintext highlighter-rouge">events</code> prop is an array of objects and that each object in that array has a certain set of properties, each of a certain type. We are specifying that the <code class="language-plaintext highlighter-rouge">events</code> prop and all of the object properties are required. Consequently, while in development mode, an error will be thrown if anything is missing or of the incorrect type.</p>
<p>If you now visit <a href="http://localhost:3000">http://localhost:3000</a> you should see a list of events displayed. Exciting, huh?</p>
<p><img src="https://res.cloudinary.com/hibbard/image/upload/v1647882402/event-manager-hooks/04-list-of-events.png" alt="Event Manager - list of events" /></p>
<h2 id="adding-some-development-tooling">Adding Some Development Tooling</h2>
<p>Now that we’re writing some JavaScript, it’s a good time to install a couple of tools to aid our development process and to ensure the quality of our code.</p>
<h3 id="eslint">ESLint</h3>
<p><a href="https://eslint.org/">ESLint</a> is a tool for identifying common errors and problematic patterns in JavaScript code. As regards code quality, this is one of the most useful tools in a JavaScript developer’s toolbox. You can install it like so:</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>npm i <span class="nt">-D</span> eslint
</code></pre></div></div>
<p>Then add the <a href="https://github.com/airbnb/javascript/tree/master/packages/eslint-config-airbnb">Airbnb config</a> to the project. This provides a set of rules corresponding to the <a href="https://github.com/airbnb/javascript">Airbnb JavaScript Style Guide</a>:</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>npm i <span class="nt">-D</span> eslint-config-airbnb
</code></pre></div></div>
<blockquote>
<p>If you are using Yarn, this will be <code class="language-plaintext highlighter-rouge">yarn add --dev eslint eslint-config-airbnb</code>.</p>
</blockquote>
<p>Next, find out what the remaining dependencies are:</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>npm info <span class="s2">"eslint-config-airbnb@latest"</span> peerDependencies
</code></pre></div></div>
<p>Outputs:</p>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span>
<span class="nl">eslint</span><span class="p">:</span> <span class="dl">'</span><span class="s1">^7.32.0 || ^8.2.0</span><span class="dl">'</span><span class="p">,</span>
<span class="dl">'</span><span class="s1">eslint-plugin-import</span><span class="dl">'</span><span class="p">:</span> <span class="dl">'</span><span class="s1">^2.25.3</span><span class="dl">'</span><span class="p">,</span>
<span class="dl">'</span><span class="s1">eslint-plugin-jsx-a11y</span><span class="dl">'</span><span class="p">:</span> <span class="dl">'</span><span class="s1">^6.5.1</span><span class="dl">'</span><span class="p">,</span>
<span class="dl">'</span><span class="s1">eslint-plugin-react</span><span class="dl">'</span><span class="p">:</span> <span class="dl">'</span><span class="s1">^7.28.0</span><span class="dl">'</span><span class="p">,</span>
<span class="dl">'</span><span class="s1">eslint-plugin-react-hooks</span><span class="dl">'</span><span class="p">:</span> <span class="dl">'</span><span class="s1">^4.3.0</span><span class="dl">'</span>
<span class="p">}</span>
</code></pre></div></div>
<p>Add the final four packages to the <code class="language-plaintext highlighter-rouge">devDependencies</code> section of <code class="language-plaintext highlighter-rouge">package.json</code> and run <code class="language-plaintext highlighter-rouge">npm i</code> (or <code class="language-plaintext highlighter-rouge">yarn install</code>) to pull them in.</p>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nl">"devDependencies"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="err">...</span><span class="w">
</span><span class="nl">"eslint-plugin-import"</span><span class="p">:</span><span class="w"> </span><span class="s2">"^2.25.3"</span><span class="p">,</span><span class="w">
</span><span class="nl">"eslint-plugin-jsx-a11y"</span><span class="p">:</span><span class="w"> </span><span class="s2">"^6.5.1"</span><span class="p">,</span><span class="w">
</span><span class="nl">"eslint-plugin-react"</span><span class="p">:</span><span class="w"> </span><span class="s2">"^7.28.0"</span><span class="p">,</span><span class="w">
</span><span class="nl">"eslint-plugin-react-hooks"</span><span class="p">:</span><span class="w"> </span><span class="s2">"^4.3.0"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>
<p>Create an <code class="language-plaintext highlighter-rouge">.eslintrc.js</code> file in the project root and add:</p>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">module</span><span class="p">.</span><span class="nx">exports</span> <span class="o">=</span> <span class="p">{</span>
<span class="na">root</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
<span class="na">extends</span><span class="p">:</span> <span class="p">[</span><span class="dl">'</span><span class="s1">airbnb</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">airbnb/hooks</span><span class="dl">'</span><span class="p">],</span>
<span class="na">rules</span><span class="p">:</span> <span class="p">{</span>
<span class="dl">'</span><span class="s1">react/jsx-filename-extension</span><span class="dl">'</span><span class="p">:</span> <span class="p">[</span><span class="mi">1</span><span class="p">,</span> <span class="p">{</span> <span class="na">extensions</span><span class="p">:</span> <span class="p">[</span><span class="dl">'</span><span class="s1">.js</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">.jsx</span><span class="dl">'</span><span class="p">]</span> <span class="p">}],</span>
<span class="dl">'</span><span class="s1">react/function-component-definition</span><span class="dl">'</span><span class="p">:</span> <span class="p">[</span>
<span class="mi">1</span><span class="p">,</span>
<span class="p">{</span> <span class="na">namedComponents</span><span class="p">:</span> <span class="dl">'</span><span class="s1">arrow-function</span><span class="dl">'</span> <span class="p">},</span>
<span class="p">],</span>
<span class="dl">'</span><span class="s1">no-console</span><span class="dl">'</span><span class="p">:</span> <span class="mi">0</span><span class="p">,</span>
<span class="dl">'</span><span class="s1">no-alert</span><span class="dl">'</span><span class="p">:</span> <span class="mi">0</span><span class="p">,</span>
<span class="p">},</span>
<span class="p">};</span>
</code></pre></div></div>
<p>This will tell ESLint to use the Airbnb ruleset we just installed and to enable the linting rules for React hooks . It will also allow files with a <code class="language-plaintext highlighter-rouge">js</code> ending to contain JSX, switch off warnings for <code class="language-plaintext highlighter-rouge">console</code> and <code class="language-plaintext highlighter-rouge">alert</code> statements and allow us to use arrow function syntax for our function components.</p>
<p>If you would prefer to enforce a different function type for function components, you can read about how to configure that rule <a href="https://github.com/yannickcr/eslint-plugin-react/blob/master/docs/rules/function-component-definition.md">here</a>.</p>
<p>You can run ESLint from the terminal:</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>./node_modules/.bin/eslint app/javascript
</code></pre></div></div>
<p>Or as an npm script:</p>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nl">"scripts"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nl">"lint"</span><span class="p">:</span><span class="w"> </span><span class="s2">"eslint app/javascript"</span><span class="w">
</span><span class="p">}</span><span class="err">,</span><span class="w">
</span></code></pre></div></div>
<p>But for the best results, you’ll probably want to integrate it into your editor. I’m using Sublime Text 3 with <a href="https://github.com/SublimeLinter/SublimeLinter">SublimeLinter</a>, <a href="https://github.com/SublimeLinter/SublimeLinter-eslint">SublimeLinter-eslint</a> and <a href="https://github.com/jonlabelle/SublimeJsPrettier">SublimeJsPrettier</a> to great effect. I also use <a href="https://github.com/prettier/eslint-config-prettier">eslint-config-prettier</a>, which turns off any linting rules that might conflict with Prettier.</p>
<p>If you’re more of a VS Code person, you might like this video by Wes Bos: <a href="https://www.youtube.com/watch?v=lHAeK8t94as">ESLint + Prettier + VS Code — The Perfect Setup</a>.</p>
<h3 id="react-developer-tools">React Developer Tools</h3>
<p>While we are looking at tooling, you might also like to take a minute to check out React’s Developer Tools. These let you inspect the React component hierarchy, including component props and state and are available as a browser extension (for <a href="https://chrome.google.com/webstore/detail/react-developer-tools/fmkadmapgofadopljbjfkapdkoienihi">Chrome</a> and <a href="https://addons.mozilla.org/en-US/firefox/addon/react-devtools/">Firefox</a>), and as a <a href="https://www.npmjs.com/package/react-devtools">standalone app</a>.</p>
<p><img src="https://res.cloudinary.com/hibbard/image/upload/v1647940149/event-manager-hooks/05-react-developer-tools.png" alt="React Developer Tools" />
You won’t need the React Developer Tools overly much if you follow along with this tutorial verbatim, but as soon as you start to try things out and deviate from what I am doing, they will be extremely helpful in understanding what is going on under the hood.</p>
<h2 id="displaying-an-event">Displaying an Event</h2>
<p>Next, let’s make the events list clickable, so that when a user selects an event, its details are displayed on the screen. For this we’re going to need React router, which will change the URL to reflect the current event and provide us with an outlet for our event information.</p>
<h3 id="react-router">React Router</h3>
<p>You can install React Router like so:</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>npm i react-router-dom@6
</code></pre></div></div>
<p>Or like so if you are using Yarn:</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>yarn add react-router-dom@6
</code></pre></div></div>
<p>As you can see, we are using the latest version of React Router (version 6). You should be aware that this library underwent a major rewrite between versions 5 and 6. If you would like to take a closer look at working with React Router 6, I recommend <a href="https://www.robinwieruch.de/react-router/">this tutorial by Robin Wieruch</a>.</p>
<p>To begin, let’s sort out the routes in <code class="language-plaintext highlighter-rouge">config/routes.rb</code>:</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">Rails</span><span class="p">.</span><span class="nf">application</span><span class="p">.</span><span class="nf">routes</span><span class="p">.</span><span class="nf">draw</span> <span class="k">do</span>
<span class="n">root</span> <span class="ss">to: </span><span class="n">redirect</span><span class="p">(</span><span class="s1">'/events'</span><span class="p">)</span>
<span class="n">get</span> <span class="s1">'events'</span><span class="p">,</span> <span class="ss">to: </span><span class="s1">'site#index'</span>
<span class="n">get</span> <span class="s1">'events/new'</span><span class="p">,</span> <span class="ss">to: </span><span class="s1">'site#index'</span>
<span class="n">get</span> <span class="s1">'events/:id'</span><span class="p">,</span> <span class="ss">to: </span><span class="s1">'site#index'</span>
<span class="n">get</span> <span class="s1">'events/:id/edit'</span><span class="p">,</span> <span class="ss">to: </span><span class="s1">'site#index'</span>
<span class="n">namespace</span> <span class="ss">:api</span> <span class="k">do</span>
<span class="n">resources</span> <span class="ss">:events</span><span class="p">,</span> <span class="ss">only: </span><span class="sx">%i[index show create destroy update]</span>
<span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>
<p>In the first line we’re pointing our root route to <code class="language-plaintext highlighter-rouge">http://localhost:3000/events</code>, this is purely for aesthetic reasons. However in the four lines that follow, you can see that we are informing Rails about the routes we will be using in our React application. This is important, as otherwise if a user requested any of these routes directly (by refreshing the page, for example), Rails would know nothing about them and would respond with a 404. Doing things this way means that Rails can simply serve our React app and let it work out which view to display.</p>
<p>Now let’s add the router to <code class="language-plaintext highlighter-rouge">app/javascript/application.js</code>:</p>
<div class="language-jsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="nx">React</span><span class="p">,</span> <span class="p">{</span> <span class="nx">StrictMode</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">react</span><span class="dl">'</span><span class="p">;</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">createRoot</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">react-dom/client</span><span class="dl">'</span><span class="p">;</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">BrowserRouter</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">react-router-dom</span><span class="dl">'</span><span class="p">;</span>
<span class="k">import</span> <span class="nx">App</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">./components/App</span><span class="dl">'</span><span class="p">;</span>
<span class="kd">const</span> <span class="nx">container</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">getElementById</span><span class="p">(</span><span class="dl">'</span><span class="s1">root</span><span class="dl">'</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">root</span> <span class="o">=</span> <span class="nx">createRoot</span><span class="p">(</span><span class="nx">container</span><span class="p">);</span>
<span class="nb">document</span><span class="p">.</span><span class="nx">addEventListener</span><span class="p">(</span><span class="dl">'</span><span class="s1">DOMContentLoaded</span><span class="dl">'</span><span class="p">,</span> <span class="p">()</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">root</span><span class="p">.</span><span class="nx">render</span><span class="p">(</span>
<span class="p"><</span><span class="nc">StrictMode</span><span class="p">></span>
<span class="p"><</span><span class="nc">BrowserRouter</span><span class="p">></span>
<span class="p"><</span><span class="nc">App</span> <span class="p">/></span>
<span class="p"></</span><span class="nc">BrowserRouter</span><span class="p">></span>
<span class="p"></</span><span class="nc">StrictMode</span><span class="p">></span>
<span class="p">);</span>
<span class="p">});</span>
</code></pre></div></div>
<p>This wraps the app in a <a href="https://reactrouterdotcom.fly.dev/docs/en/v6/api#browserrouter"><BrowserRouter> component</a>, that uses the HTML5 history API to keep the UI in sync with the URL.</p>
<p>A small change is necessary in <code class="language-plaintext highlighter-rouge">App.js</code>:</p>
<div class="language-jsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="nx">React</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">react</span><span class="dl">'</span><span class="p">;</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">Routes</span><span class="p">,</span> <span class="nx">Route</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">react-router-dom</span><span class="dl">'</span><span class="p">;</span>
<span class="k">import</span> <span class="nx">Editor</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">./Editor</span><span class="dl">'</span><span class="p">;</span>
<span class="kd">const</span> <span class="nx">App</span> <span class="o">=</span> <span class="p">()</span> <span class="o">=></span> <span class="p">(</span>
<span class="p"><</span><span class="nc">Routes</span><span class="p">></span>
<span class="p"><</span><span class="nc">Route</span> <span class="na">path</span><span class="p">=</span><span class="s">"events/*"</span> <span class="na">element</span><span class="p">=</span><span class="si">{</span><span class="p"><</span><span class="nc">Editor</span> <span class="p">/></span><span class="si">}</span> <span class="p">/></span>
<span class="p"></</span><span class="nc">Routes</span><span class="p">></span>
<span class="p">);</span>
<span class="k">export</span> <span class="k">default</span> <span class="nx">App</span><span class="p">;</span>
</code></pre></div></div>
<p>Instead of rendering our <code class="language-plaintext highlighter-rouge"><Editor></code> component directly, we will now use a <a href="https://reactrouterdotcom.fly.dev/docs/en/v6/api#routes-and-route"><Route> component</a> to render it whenever the browser’s URL begins with <code class="language-plaintext highlighter-rouge">events/</code>.</p>
<p>To make the event display in the correct place, we need to use a further route. In the <code class="language-plaintext highlighter-rouge"><Editor></code> component:</p>
<div class="language-jsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">...</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">Routes</span><span class="p">,</span> <span class="nx">Route</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">react-router-dom</span><span class="dl">'</span><span class="p">;</span>
<span class="k">import</span> <span class="nx">Event</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">./Event</span><span class="dl">'</span><span class="p">;</span>
<span class="kd">const</span> <span class="nx">Editor</span> <span class="o">=</span> <span class="p">()</span> <span class="o">=></span> <span class="p">{</span>
<span class="p">...</span>
<span class="k">return</span> <span class="p">(</span>
<span class="p"><></span>
<span class="p"><</span><span class="nc">Header</span> <span class="p">/></span>
<span class="si">{</span><span class="nx">isError</span> <span class="o">&&</span> <span class="p"><</span><span class="nt">p</span><span class="p">></span>Something went wrong. Check the console.<span class="p"></</span><span class="nt">p</span><span class="p">></span><span class="si">}</span>
<span class="si">{</span><span class="nx">isLoading</span> <span class="p">?</span> <span class="p">(</span>
<span class="p"><</span><span class="nt">p</span><span class="p">></span>Loading...<span class="p"></</span><span class="nt">p</span><span class="p">></span>
<span class="p">)</span> <span class="p">:</span> <span class="p">(</span>
<span class="p"><></span>
<span class="p"><</span><span class="nc">EventList</span> <span class="na">events</span><span class="p">=</span><span class="si">{</span><span class="nx">events</span><span class="si">}</span> <span class="p">/></span>
<span class="p"><</span><span class="nc">Routes</span><span class="p">></span>
<span class="p"><</span><span class="nc">Route</span> <span class="na">path</span><span class="p">=</span><span class="s">":id"</span> <span class="na">element</span><span class="p">=</span><span class="si">{</span><span class="p"><</span><span class="nc">Event</span> <span class="na">events</span><span class="p">=</span><span class="si">{</span><span class="nx">events</span><span class="si">}</span> <span class="p">/></span><span class="si">}</span> <span class="p">/></span>
<span class="p"></</span><span class="nc">Routes</span><span class="p">></span>
<span class="p"></></span>
<span class="p">)</span><span class="si">}</span>
<span class="p"></></span>
<span class="p">);</span>
<span class="p">};</span>
<span class="k">export</span> <span class="k">default</span> <span class="nx">Editor</span><span class="p">;</span>
</code></pre></div></div>
<p>As in the <code class="language-plaintext highlighter-rouge"><App></code> component, we are using a <code class="language-plaintext highlighter-rouge"><Route></code> component, whose path we set to <code class="language-plaintext highlighter-rouge">:id</code>. This is known as a <a href="https://reactrouter.com/docs/en/v6/getting-started/concepts#dynamic-segment">dynamic segment</a> which will match the ID of the current event.</p>
<p>This means that given a URL such as <code class="language-plaintext highlighter-rouge">http://localhost:3000/events/7</code>, the following will happen:</p>
<ul>
<li><code class="language-plaintext highlighter-rouge">application.js</code> will render our <code class="language-plaintext highlighter-rouge"><App></code> component, wrapped in a <code class="language-plaintext highlighter-rouge"><BrowserRouter></code></li>
<li>In the <code class="language-plaintext highlighter-rouge"><App></code> component, the <code class="language-plaintext highlighter-rouge"><Route></code> component will match the <code class="language-plaintext highlighter-rouge">events/</code> part of the URL and render the <code class="language-plaintext highlighter-rouge"><Editor></code> component.</li>
<li>In the <code class="language-plaintext highlighter-rouge"><Editor></code> component, the <code class="language-plaintext highlighter-rouge"><Route></code> component will match the remainder of the URL (i.e. <code class="language-plaintext highlighter-rouge">7</code>) and render the <code class="language-plaintext highlighter-rouge"><Event></code> component, passing it the list of events we fetched previously.</li>
<li>Inside the <code class="language-plaintext highlighter-rouge"><Event></code> component, the <code class="language-plaintext highlighter-rouge">:id</code> section of the URL will be available to our code.</li>
</ul>
<p>Take a moment to ensure you understand everything that is going on here, then move on to the next section.</p>
<h3 id="the-event-component">The <code class="language-plaintext highlighter-rouge"><Event></code> Component</h3>
<p>Next, we’ll need an <code class="language-plaintext highlighter-rouge"><Event></code> component to display the event.</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">touch </span>app/javascript/components/Event.js
</code></pre></div></div>
<p>Then add:</p>
<div class="language-jsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="nx">React</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">react</span><span class="dl">'</span><span class="p">;</span>
<span class="k">import</span> <span class="nx">PropTypes</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">prop-types</span><span class="dl">'</span><span class="p">;</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">useParams</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">react-router-dom</span><span class="dl">'</span><span class="p">;</span>
<span class="kd">const</span> <span class="nx">Event</span> <span class="o">=</span> <span class="p">({</span> <span class="nx">events</span> <span class="p">})</span> <span class="o">=></span> <span class="p">{</span>
<span class="kd">const</span> <span class="p">{</span> <span class="nx">id</span> <span class="p">}</span> <span class="o">=</span> <span class="nx">useParams</span><span class="p">();</span>
<span class="kd">const</span> <span class="nx">event</span> <span class="o">=</span> <span class="nx">events</span><span class="p">.</span><span class="nx">find</span><span class="p">((</span><span class="nx">e</span><span class="p">)</span> <span class="o">=></span> <span class="nx">e</span><span class="p">.</span><span class="nx">id</span> <span class="o">===</span> <span class="nb">Number</span><span class="p">(</span><span class="nx">id</span><span class="p">));</span>
<span class="k">return</span> <span class="p">(</span>
<span class="p"><></span>
<span class="p"><</span><span class="nt">h2</span><span class="p">></span>
<span class="si">{</span><span class="nx">event</span><span class="p">.</span><span class="nx">event_date</span><span class="si">}</span>
<span class="si">{</span><span class="dl">'</span><span class="s1"> - </span><span class="dl">'</span><span class="si">}</span>
<span class="si">{</span><span class="nx">event</span><span class="p">.</span><span class="nx">event_type</span><span class="si">}</span>
<span class="p"></</span><span class="nt">h2</span><span class="p">></span>
<span class="p"><</span><span class="nt">ul</span><span class="p">></span>
<span class="p"><</span><span class="nt">li</span><span class="p">></span>
<span class="p"><</span><span class="nt">strong</span><span class="p">></span>Type:<span class="p"></</span><span class="nt">strong</span><span class="p">></span> <span class="si">{</span><span class="nx">event</span><span class="p">.</span><span class="nx">event_type</span><span class="si">}</span>
<span class="p"></</span><span class="nt">li</span><span class="p">></span>
<span class="p"><</span><span class="nt">li</span><span class="p">></span>
<span class="p"><</span><span class="nt">strong</span><span class="p">></span>Date:<span class="p"></</span><span class="nt">strong</span><span class="p">></span> <span class="si">{</span><span class="nx">event</span><span class="p">.</span><span class="nx">event_date</span><span class="si">}</span>
<span class="p"></</span><span class="nt">li</span><span class="p">></span>
<span class="p"><</span><span class="nt">li</span><span class="p">></span>
<span class="p"><</span><span class="nt">strong</span><span class="p">></span>Title:<span class="p"></</span><span class="nt">strong</span><span class="p">></span> <span class="si">{</span><span class="nx">event</span><span class="p">.</span><span class="nx">title</span><span class="si">}</span>
<span class="p"></</span><span class="nt">li</span><span class="p">></span>
<span class="p"><</span><span class="nt">li</span><span class="p">></span>
<span class="p"><</span><span class="nt">strong</span><span class="p">></span>Speaker:<span class="p"></</span><span class="nt">strong</span><span class="p">></span> <span class="si">{</span><span class="nx">event</span><span class="p">.</span><span class="nx">speaker</span><span class="si">}</span>
<span class="p"></</span><span class="nt">li</span><span class="p">></span>
<span class="p"><</span><span class="nt">li</span><span class="p">></span>
<span class="p"><</span><span class="nt">strong</span><span class="p">></span>Host:<span class="p"></</span><span class="nt">strong</span><span class="p">></span> <span class="si">{</span><span class="nx">event</span><span class="p">.</span><span class="nx">host</span><span class="si">}</span>
<span class="p"></</span><span class="nt">li</span><span class="p">></span>
<span class="p"><</span><span class="nt">li</span><span class="p">></span>
<span class="p"><</span><span class="nt">strong</span><span class="p">></span>Published:<span class="p"></</span><span class="nt">strong</span><span class="p">></span> <span class="si">{</span><span class="nx">event</span><span class="p">.</span><span class="nx">published</span> <span class="p">?</span> <span class="dl">'</span><span class="s1">yes</span><span class="dl">'</span> <span class="p">:</span> <span class="dl">'</span><span class="s1">no</span><span class="dl">'</span><span class="si">}</span>
<span class="p"></</span><span class="nt">li</span><span class="p">></span>
<span class="p"></</span><span class="nt">ul</span><span class="p">></span>
<span class="p"></></span>
<span class="p">);</span>
<span class="p">};</span>
<span class="nx">Event</span><span class="p">.</span><span class="nx">propTypes</span> <span class="o">=</span> <span class="p">{</span>
<span class="na">events</span><span class="p">:</span> <span class="nx">PropTypes</span><span class="p">.</span><span class="nx">arrayOf</span><span class="p">(</span>
<span class="nx">PropTypes</span><span class="p">.</span><span class="nx">shape</span><span class="p">({</span>
<span class="na">id</span><span class="p">:</span> <span class="nx">PropTypes</span><span class="p">.</span><span class="nx">number</span><span class="p">.</span><span class="nx">isRequired</span><span class="p">,</span>
<span class="na">event_type</span><span class="p">:</span> <span class="nx">PropTypes</span><span class="p">.</span><span class="nx">string</span><span class="p">.</span><span class="nx">isRequired</span><span class="p">,</span>
<span class="na">event_date</span><span class="p">:</span> <span class="nx">PropTypes</span><span class="p">.</span><span class="nx">string</span><span class="p">.</span><span class="nx">isRequired</span><span class="p">,</span>
<span class="na">title</span><span class="p">:</span> <span class="nx">PropTypes</span><span class="p">.</span><span class="nx">string</span><span class="p">.</span><span class="nx">isRequired</span><span class="p">,</span>
<span class="na">speaker</span><span class="p">:</span> <span class="nx">PropTypes</span><span class="p">.</span><span class="nx">string</span><span class="p">.</span><span class="nx">isRequired</span><span class="p">,</span>
<span class="na">host</span><span class="p">:</span> <span class="nx">PropTypes</span><span class="p">.</span><span class="nx">string</span><span class="p">.</span><span class="nx">isRequired</span><span class="p">,</span>
<span class="na">published</span><span class="p">:</span> <span class="nx">PropTypes</span><span class="p">.</span><span class="nx">bool</span><span class="p">.</span><span class="nx">isRequired</span><span class="p">,</span>
<span class="p">})</span>
<span class="p">).</span><span class="nx">isRequired</span><span class="p">,</span>
<span class="p">};</span>
<span class="k">export</span> <span class="k">default</span> <span class="nx">Event</span><span class="p">;</span>
</code></pre></div></div>
<p>Here we are importing the <a href="https://reactrouterdotcom.fly.dev/docs/en/v6/api#useparams">useParams hook</a> from React Router. This hook gives us access to an object containing the dynamic params from the current URL that were matched by the <code class="language-plaintext highlighter-rouge"><Route path></code> (<code class="language-plaintext highlighter-rouge">:id</code> in our case).</p>
<p>We then grab the ID of the current event using <a href="https://javascript.info/destructuring-assignment#object-destructuring">destructuring assignment</a> and use that ID to filter the list of events we passed as props, and find the event we want to display.</p>
<p>I would have preferred to only pass the component the event it needs to display (as opposed to all of them), but the way that React Router now works, made this rather tricky. I did also consider sticking all of the events into <a href="https://reactjs.org/docs/context.html">Context</a>, but that would have increased the complexity of an already long tutorial and would have been overkill in this case. Nonetheless, be aware that this is an option open to you.</p>
<p>The rest of the code is hopefully easy enough to understand, with some JSX being returned and the same prop validation happening as before.</p>
<h3 id="making-events-clickable">Making Events Clickable</h3>
<p>Finally, let’s make the list of events in <code class="language-plaintext highlighter-rouge"><EventList></code> clickable. When clicked, they should navigate to <code class="language-plaintext highlighter-rouge">/events/:id</code>.</p>
<div class="language-jsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">Link</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">react-router-dom</span><span class="dl">'</span><span class="p">;</span>
<span class="p">...</span>
<span class="kd">const</span> <span class="nx">renderEvents</span> <span class="o">=</span> <span class="p">(</span><span class="nx">eventArray</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">eventArray</span><span class="p">.</span><span class="nx">sort</span><span class="p">((</span><span class="nx">a</span><span class="p">,</span> <span class="nx">b</span><span class="p">)</span> <span class="o">=></span> <span class="k">new</span> <span class="nb">Date</span><span class="p">(</span><span class="nx">b</span><span class="p">.</span><span class="nx">event_date</span><span class="p">)</span> <span class="o">-</span> <span class="k">new</span> <span class="nb">Date</span><span class="p">(</span><span class="nx">a</span><span class="p">.</span><span class="nx">event_date</span><span class="p">));</span>
<span class="k">return</span> <span class="nx">eventArray</span><span class="p">.</span><span class="nx">map</span><span class="p">((</span><span class="nx">event</span><span class="p">)</span> <span class="o">=></span> <span class="p">(</span>
<span class="p"><</span><span class="nt">li</span> <span class="na">key</span><span class="p">=</span><span class="si">{</span><span class="nx">event</span><span class="p">.</span><span class="nx">id</span><span class="si">}</span><span class="p">></span>
<span class="p"><</span><span class="nc">Link</span> <span class="na">to</span><span class="p">=</span><span class="si">{</span><span class="s2">`/events/</span><span class="p">${</span><span class="nx">event</span><span class="p">.</span><span class="nx">id</span><span class="p">}</span><span class="s2">`</span><span class="si">}</span><span class="p">></span>
<span class="si">{</span><span class="nx">event</span><span class="p">.</span><span class="nx">event_date</span><span class="si">}</span>
<span class="si">{</span><span class="dl">'</span><span class="s1"> - </span><span class="dl">'</span><span class="si">}</span>
<span class="si">{</span><span class="nx">event</span><span class="p">.</span><span class="nx">event_type</span><span class="si">}</span>
<span class="p"></</span><span class="nc">Link</span><span class="p">></span>
<span class="p"></</span><span class="nt">li</span><span class="p">></span>
<span class="p">));</span>
<span class="p">};</span>
</code></pre></div></div>
<p>Here, we are making use of React router’s <a href="https://reactrouterdotcom.fly.dev/docs/en/v6/api#link"><Link> component</a> to create the navigation around our application.</p>
<p>And now when you click on a link, the correct event should display.</p>
<p><img src="https://res.cloudinary.com/hibbard/image/upload/v1647951493/event-manager-hooks/06-displaying-an-event.png" alt="Event Manager - displaying an event" /></p>
<h2 id="adding-some-styling">Adding Some Styling</h2>
<p>The app looks pretty ugly right now, so let’s brighten it up a little. Create a file named <code class="language-plaintext highlighter-rouge">App.css</code>:</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">touch </span>app/javascript/components/App.css
</code></pre></div></div>
<p>And add the following styles:</p>
<div class="language-css highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">body</span><span class="o">,</span> <span class="nt">html</span><span class="o">,</span> <span class="nt">div</span><span class="o">,</span> <span class="nt">blockquote</span><span class="o">,</span> <span class="nt">img</span><span class="o">,</span> <span class="nt">label</span><span class="o">,</span> <span class="nt">p</span><span class="o">,</span> <span class="nt">h1</span><span class="o">,</span> <span class="nt">h2</span><span class="o">,</span> <span class="nt">h3</span><span class="o">,</span> <span class="nt">h4</span><span class="o">,</span> <span class="nt">h5</span><span class="o">,</span> <span class="nt">h6</span><span class="o">,</span> <span class="nt">pre</span><span class="o">,</span> <span class="nt">ul</span><span class="o">,</span> <span class="nt">ol</span><span class="o">,</span> <span class="nt">li</span><span class="o">,</span> <span class="nt">dl</span><span class="o">,</span> <span class="nt">dt</span><span class="o">,</span> <span class="nt">dd</span><span class="o">,</span> <span class="nt">form</span><span class="o">,</span> <span class="nt">a</span><span class="o">,</span> <span class="nt">fieldset</span><span class="o">,</span> <span class="nt">input</span><span class="o">,</span> <span class="nt">th</span><span class="o">,</span> <span class="nt">td</span> <span class="p">{</span>
<span class="nl">margin</span><span class="p">:</span> <span class="m">0</span><span class="p">;</span>
<span class="nl">padding</span><span class="p">:</span> <span class="m">0</span><span class="p">;</span>
<span class="p">}</span>
<span class="nt">ul</span><span class="o">,</span> <span class="nt">ol</span> <span class="p">{</span>
<span class="nl">list-style</span><span class="p">:</span> <span class="nb">none</span><span class="p">;</span>
<span class="p">}</span>
<span class="nt">body</span> <span class="p">{</span>
<span class="nl">font-family</span><span class="p">:</span> <span class="n">Roboto</span><span class="p">;</span>
<span class="nl">font-size</span><span class="p">:</span> <span class="m">16px</span><span class="p">;</span>
<span class="nl">line-height</span><span class="p">:</span> <span class="m">28px</span><span class="p">;</span>
<span class="p">}</span>
<span class="nt">header</span> <span class="p">{</span>
<span class="nl">background</span><span class="p">:</span> <span class="m">#f57011</span><span class="p">;</span>
<span class="nl">height</span><span class="p">:</span> <span class="m">60px</span><span class="p">;</span>
<span class="p">}</span>
<span class="nt">header</span> <span class="nt">h1</span><span class="o">,</span> <span class="nt">header</span> <span class="nt">h1</span> <span class="nt">a</span><span class="p">{</span>
<span class="nl">display</span><span class="p">:</span> <span class="n">inline-block</span><span class="p">;</span>
<span class="nl">font-family</span><span class="p">:</span> <span class="s1">"Maven Pro"</span><span class="p">;</span>
<span class="nl">font-size</span><span class="p">:</span> <span class="m">28px</span><span class="p">;</span>
<span class="nl">font-weight</span><span class="p">:</span> <span class="m">500</span><span class="p">;</span>
<span class="nl">color</span><span class="p">:</span> <span class="no">white</span><span class="p">;</span>
<span class="nl">padding</span><span class="p">:</span> <span class="m">14px</span> <span class="m">5%</span><span class="p">;</span>
<span class="nl">text-decoration</span><span class="p">:</span> <span class="nb">none</span><span class="p">;</span>
<span class="p">}</span>
<span class="nt">header</span> <span class="nt">h1</span><span class="nd">:hover</span> <span class="p">{</span>
<span class="nl">text-decoration</span><span class="p">:</span> <span class="nb">underline</span><span class="p">;</span>
<span class="p">}</span>
<span class="nc">.grid</span> <span class="p">{</span>
<span class="nl">display</span><span class="p">:</span> <span class="n">grid</span><span class="p">;</span>
<span class="py">grid-gap</span><span class="p">:</span> <span class="m">50px</span><span class="p">;</span>
<span class="py">grid-template-columns</span><span class="p">:</span> <span class="n">minmax</span><span class="p">(</span><span class="m">250px</span><span class="p">,</span> <span class="m">20%</span><span class="p">)</span> <span class="nb">auto</span><span class="p">;</span>
<span class="nl">margin</span><span class="p">:</span> <span class="m">25px</span> <span class="nb">auto</span><span class="p">;</span>
<span class="nl">width</span><span class="p">:</span> <span class="m">90%</span><span class="p">;</span>
<span class="nl">height</span><span class="p">:</span> <span class="n">calc</span><span class="p">(</span><span class="m">100vh</span> <span class="n">-</span> <span class="m">145px</span><span class="p">);</span>
<span class="p">}</span>
<span class="nc">.eventList</span> <span class="p">{</span>
<span class="nl">background</span><span class="p">:</span> <span class="m">#f6f6f6</span><span class="p">;</span>
<span class="nl">padding</span><span class="p">:</span> <span class="m">16px</span><span class="p">;</span>
<span class="p">}</span>
<span class="nc">.eventList</span> <span class="nt">h2</span> <span class="p">{</span>
<span class="nl">font-size</span><span class="p">:</span> <span class="m">20px</span><span class="p">;</span>
<span class="nl">padding</span><span class="p">:</span> <span class="m">8px</span> <span class="m">6px</span> <span class="m">10px</span><span class="p">;</span>
<span class="p">}</span>
<span class="nc">.eventContainer</span> <span class="p">{</span>
<span class="nl">font-size</span><span class="p">:</span> <span class="m">15px</span><span class="p">;</span>
<span class="nl">line-height</span><span class="p">:</span> <span class="m">35px</span><span class="p">;</span>
<span class="p">}</span>
<span class="nc">.eventContainer</span> <span class="nt">h2</span> <span class="p">{</span>
<span class="nl">margin-bottom</span><span class="p">:</span> <span class="m">10px</span><span class="p">;</span>
<span class="p">}</span>
<span class="nc">.eventList</span> <span class="nt">li</span><span class="nd">:hover</span><span class="o">,</span> <span class="nt">a</span><span class="nc">.active</span> <span class="p">{</span>
<span class="nl">background</span><span class="p">:</span> <span class="m">#f8e5ce</span><span class="p">;</span>
<span class="p">}</span>
<span class="nc">.eventList</span> <span class="nt">a</span> <span class="p">{</span>
<span class="nl">display</span><span class="p">:</span> <span class="nb">block</span><span class="p">;</span>
<span class="nl">color</span><span class="p">:</span> <span class="no">black</span><span class="p">;</span>
<span class="nl">text-decoration</span><span class="p">:</span> <span class="nb">none</span><span class="p">;</span>
<span class="nl">border-bottom</span><span class="p">:</span> <span class="m">1px</span> <span class="nb">solid</span> <span class="m">#dddddd</span><span class="p">;</span>
<span class="nl">padding</span><span class="p">:</span> <span class="m">8px</span> <span class="m">6px</span> <span class="m">10px</span><span class="p">;</span>
<span class="nl">outline</span><span class="p">:</span> <span class="m">0</span><span class="p">;</span>
<span class="p">}</span>
<span class="nc">.eventList</span> <span class="nt">h2</span> <span class="o">></span> <span class="nt">a</span> <span class="p">{</span>
<span class="nl">color</span><span class="p">:</span> <span class="m">#236fff</span><span class="p">;</span>
<span class="nl">font-size</span><span class="p">:</span> <span class="m">15px</span><span class="p">;</span>
<span class="nl">float</span><span class="p">:</span> <span class="nb">right</span><span class="p">;</span>
<span class="nl">font-weight</span><span class="p">:</span> <span class="nb">normal</span><span class="p">;</span>
<span class="nl">border-bottom</span><span class="p">:</span> <span class="nb">none</span><span class="p">;</span>
<span class="nl">padding</span><span class="p">:</span> <span class="m">0px</span><span class="p">;</span>
<span class="p">}</span>
<span class="nc">.eventForm</span> <span class="p">{</span>
<span class="nl">margin-top</span><span class="p">:</span> <span class="m">15px</span><span class="p">;</span>
<span class="p">}</span>
<span class="nt">label</span> <span class="o">></span> <span class="nt">strong</span> <span class="p">{</span>
<span class="nl">display</span><span class="p">:</span> <span class="n">inline-block</span><span class="p">;</span>
<span class="nl">vertical-align</span><span class="p">:</span> <span class="nb">top</span><span class="p">;</span>
<span class="nl">text-align</span><span class="p">:</span> <span class="nb">right</span><span class="p">;</span>
<span class="nl">width</span><span class="p">:</span> <span class="m">100px</span><span class="p">;</span>
<span class="nl">margin-right</span><span class="p">:</span> <span class="m">6px</span><span class="p">;</span>
<span class="nl">font-size</span><span class="p">:</span> <span class="m">15px</span><span class="p">;</span>
<span class="p">}</span>
<span class="nt">input</span><span class="o">,</span> <span class="nt">textarea</span> <span class="p">{</span>
<span class="nl">padding</span><span class="p">:</span> <span class="m">2px</span> <span class="m">0</span> <span class="m">3px</span> <span class="m">3px</span><span class="p">;</span>
<span class="nl">width</span><span class="p">:</span> <span class="m">400px</span><span class="p">;</span>
<span class="nl">margin-bottom</span><span class="p">:</span> <span class="m">15px</span><span class="p">;</span>
<span class="nl">box-sizing</span><span class="p">:</span> <span class="n">border-box</span><span class="p">;</span>
<span class="p">}</span>
<span class="nt">input</span><span class="o">[</span><span class="nt">type</span><span class="o">=</span><span class="s1">"checkbox"</span><span class="o">]</span> <span class="p">{</span>
<span class="nl">width</span><span class="p">:</span> <span class="m">13px</span><span class="p">;</span>
<span class="p">}</span>
<span class="nt">button</span><span class="o">[</span><span class="nt">type</span><span class="o">=</span><span class="s1">"submit"</span><span class="o">]</span> <span class="p">{</span>
<span class="nl">background</span><span class="p">:</span> <span class="m">#f57011</span><span class="p">;</span>
<span class="nl">border</span><span class="p">:</span> <span class="nb">none</span><span class="p">;</span>
<span class="nl">padding</span><span class="p">:</span> <span class="m">5px</span> <span class="m">25px</span> <span class="m">8px</span><span class="p">;</span>
<span class="nl">font-weight</span><span class="p">:</span> <span class="m">500</span><span class="p">;</span>
<span class="nl">color</span><span class="p">:</span> <span class="no">white</span><span class="p">;</span>
<span class="nl">cursor</span><span class="p">:</span> <span class="nb">pointer</span><span class="p">;</span>
<span class="nl">margin</span><span class="p">:</span> <span class="m">10px</span> <span class="m">0</span> <span class="m">0</span> <span class="m">106px</span><span class="p">;</span>
<span class="p">}</span>
<span class="nc">.errors</span> <span class="p">{</span>
<span class="nl">border</span><span class="p">:</span> <span class="m">1px</span> <span class="nb">solid</span> <span class="no">red</span><span class="p">;</span>
<span class="nl">border-radius</span><span class="p">:</span> <span class="m">5px</span><span class="p">;</span>
<span class="nl">margin</span><span class="p">:</span> <span class="m">20px</span> <span class="m">0</span> <span class="m">35px</span> <span class="m">0</span><span class="p">;</span>
<span class="nl">width</span><span class="p">:</span> <span class="m">513px</span><span class="p">;</span>
<span class="p">}</span>
<span class="nc">.errors</span> <span class="nt">h3</span> <span class="p">{</span>
<span class="nl">background</span><span class="p">:</span> <span class="no">red</span><span class="p">;</span>
<span class="nl">color</span><span class="p">:</span> <span class="no">white</span><span class="p">;</span>
<span class="nl">padding</span><span class="p">:</span> <span class="m">10px</span><span class="p">;</span>
<span class="nl">font-size</span><span class="p">:</span> <span class="m">15px</span><span class="p">;</span>
<span class="p">}</span>
<span class="nc">.errors</span> <span class="nt">ul</span> <span class="nt">li</span> <span class="p">{</span>
<span class="nl">list-style-type</span><span class="p">:</span> <span class="nb">none</span><span class="p">;</span>
<span class="nl">margin</span><span class="p">:</span> <span class="m">0</span><span class="p">;</span>
<span class="nl">padding</span><span class="p">:</span> <span class="m">8px</span> <span class="m">0</span> <span class="m">8px</span> <span class="m">10px</span><span class="p">;</span>
<span class="nl">border-top</span><span class="p">:</span> <span class="nb">solid</span> <span class="m">1px</span> <span class="no">pink</span><span class="p">;</span>
<span class="nl">font-size</span><span class="p">:</span> <span class="m">12px</span><span class="p">;</span>
<span class="nl">font-weight</span><span class="p">:</span> <span class="m">0.9</span><span class="p">;</span>
<span class="p">}</span>
<span class="nt">button</span><span class="nc">.delete</span> <span class="p">{</span>
<span class="nl">background</span><span class="p">:</span> <span class="nb">none</span> <span class="cp">!important</span><span class="p">;</span>
<span class="nl">border</span><span class="p">:</span> <span class="nb">none</span><span class="p">;</span>
<span class="nl">padding</span><span class="p">:</span> <span class="m">0</span> <span class="cp">!important</span><span class="p">;</span>
<span class="nl">margin-left</span><span class="p">:</span> <span class="m">10px</span><span class="p">;</span>
<span class="nl">cursor</span><span class="p">:</span> <span class="nb">pointer</span><span class="p">;</span>
<span class="nl">color</span><span class="p">:</span> <span class="m">#236fff</span><span class="p">;</span>
<span class="nl">font-size</span><span class="p">:</span> <span class="m">15px</span><span class="p">;</span>
<span class="nl">font-weight</span><span class="p">:</span> <span class="nb">normal</span><span class="p">;</span>
<span class="nl">text-decoration</span><span class="p">:</span> <span class="nb">none</span><span class="p">;</span>
<span class="p">}</span>
<span class="nt">button</span><span class="nc">.delete</span><span class="nd">:hover</span> <span class="p">{</span>
<span class="nl">text-decoration</span><span class="p">:</span> <span class="nb">underline</span><span class="p">;</span>
<span class="p">}</span>
<span class="nt">h2</span> <span class="nt">a</span> <span class="p">{</span>
<span class="nl">color</span><span class="p">:</span> <span class="m">#236fff</span><span class="p">;</span>
<span class="nl">font-size</span><span class="p">:</span> <span class="m">15px</span><span class="p">;</span>
<span class="nl">font-weight</span><span class="p">:</span> <span class="nb">normal</span><span class="p">;</span>
<span class="nl">margin</span><span class="p">:</span> <span class="m">3px</span> <span class="m">12px</span> <span class="m">0</span> <span class="m">12px</span><span class="p">;</span>
<span class="nl">text-decoration</span><span class="p">:</span> <span class="nb">none</span><span class="p">;</span>
<span class="p">}</span>
<span class="nt">h2</span> <span class="nt">a</span><span class="nd">:hover</span> <span class="p">{</span>
<span class="nl">text-decoration</span><span class="p">:</span> <span class="nb">underline</span><span class="p">;</span>
<span class="p">}</span>
<span class="nc">.form-actions</span> <span class="nt">a</span> <span class="p">{</span>
<span class="nl">color</span><span class="p">:</span> <span class="m">#236fff</span><span class="p">;</span>
<span class="nl">font-size</span><span class="p">:</span> <span class="m">15px</span><span class="p">;</span>
<span class="nl">margin</span><span class="p">:</span> <span class="m">3px</span> <span class="m">12px</span> <span class="m">0</span> <span class="m">12px</span><span class="p">;</span>
<span class="nl">text-decoration</span><span class="p">:</span> <span class="nb">none</span><span class="p">;</span>
<span class="p">}</span>
<span class="nc">.form-actions</span> <span class="nt">a</span><span class="nd">:hover</span> <span class="p">{</span>
<span class="nl">text-decoration</span><span class="p">:</span> <span class="nb">underline</span><span class="p">;</span>
<span class="p">}</span>
<span class="nt">input</span><span class="nc">.search</span> <span class="p">{</span>
<span class="nl">width</span><span class="p">:</span> <span class="m">92%</span><span class="p">;</span>
<span class="nl">margin</span><span class="p">:</span> <span class="m">15px</span> <span class="m">2px</span><span class="p">;</span>
<span class="nl">padding</span><span class="p">:</span> <span class="m">4px</span> <span class="m">0</span> <span class="m">6px</span> <span class="m">6px</span><span class="p">;</span>
<span class="p">}</span>
<span class="nc">.loading</span> <span class="p">{</span>
<span class="nl">height</span><span class="p">:</span> <span class="n">calc</span><span class="p">(</span><span class="m">100vh</span> <span class="n">-</span> <span class="m">60px</span><span class="p">);</span>
<span class="nl">display</span><span class="p">:</span> <span class="n">grid</span><span class="p">;</span>
<span class="nl">justify-content</span><span class="p">:</span> <span class="nb">center</span><span class="p">;</span>
<span class="nl">align-content</span><span class="p">:</span> <span class="nb">center</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>
<blockquote>
<p>Please note that these are all of the styles we will need in the app. Listing them all in one go is intended to keep the article a tad shorter.</p>
</blockquote>
<p>Here, we’re using a small <a href="https://code.tutsplus.com/tutorials/quick-tip-create-your-own-simple-resetcss-file--net-206">custom reset</a> and the goodness of CSS grid for our layout. If you’re unfamiliar with CSS grid, there’s a good tutorial here: <a href="https://medialoot.com/blog/a-beginners-guide-to-css-grid-layout/">A Beginners Guide to CSS Grid Layout</a></p>
<p>Import our styles in <code class="language-plaintext highlighter-rouge">App.js</code>:</p>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="dl">'</span><span class="s1">./App.css</span><span class="dl">'</span><span class="p">;</span>
</code></pre></div></div>
<p>Next, alter the markup in the <code class="language-plaintext highlighter-rouge"><Editor></code> component:</p>
<div class="language-jsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">return</span> <span class="p">(</span>
<span class="p"><></span>
<span class="p"><</span><span class="nc">Header</span> <span class="p">/></span>
<span class="p"><</span><span class="nt">div</span> <span class="na">className</span><span class="p">=</span><span class="s">"grid"</span><span class="p">></span>
<span class="si">{</span><span class="nx">isError</span> <span class="o">&&</span> <span class="p"><</span><span class="nt">p</span><span class="p">></span>Something went wrong. Check the console.<span class="p"></</span><span class="nt">p</span><span class="p">></span><span class="si">}</span>
<span class="si">{</span><span class="nx">isLoading</span> <span class="p">?</span> <span class="p">(</span>
<span class="p"><</span><span class="nt">p</span> <span class="na">className</span><span class="p">=</span><span class="s">'loading'</span><span class="p">></span>Loading...<span class="p"></</span><span class="nt">p</span><span class="p">></span>
<span class="p">)</span> <span class="p">:</span> <span class="p">(</span>
<span class="p"><></span>
<span class="p"><</span><span class="nc">EventList</span> <span class="na">events</span><span class="p">=</span><span class="si">{</span><span class="nx">events</span><span class="si">}</span> <span class="p">/></span>
<span class="p"><</span><span class="nc">Routes</span><span class="p">></span>
<span class="p"><</span><span class="nc">Route</span> <span class="na">path</span><span class="p">=</span><span class="s">":id"</span> <span class="na">element</span><span class="p">=</span><span class="si">{</span><span class="p"><</span><span class="nc">Event</span> <span class="na">events</span><span class="p">=</span><span class="si">{</span><span class="nx">events</span><span class="si">}</span> <span class="p">/></span><span class="si">}</span> <span class="p">/></span>
<span class="p"></</span><span class="nc">Routes</span><span class="p">></span>
<span class="p"></></span>
<span class="p">)</span><span class="si">}</span>
<span class="p"></</span><span class="nt">div</span><span class="p">></span>
<span class="p"></></span>
<span class="p">);</span>
</code></pre></div></div>
<p>The <code class="language-plaintext highlighter-rouge"><EventList></code> component:</p>
<div class="language-jsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">return</span> <span class="p">(</span>
<span class="p"><</span><span class="nt">section</span> <span class="na">className</span><span class="p">=</span><span class="s">"eventList"</span><span class="p">></span>
<span class="p"><</span><span class="nt">h2</span><span class="p">></span>Events<span class="p"></</span><span class="nt">h2</span><span class="p">></span>
<span class="p"><</span><span class="nt">ul</span><span class="p">></span><span class="si">{</span><span class="nx">renderEvents</span><span class="p">(</span><span class="nx">events</span><span class="p">)</span><span class="si">}</span><span class="p"></</span><span class="nt">ul</span><span class="p">></span>
<span class="p"></</span><span class="nt">section</span><span class="p">></span>
<span class="p">);</span>
</code></pre></div></div>
<p>And the <code class="language-plaintext highlighter-rouge"><Event></code> component:</p>
<div class="language-jsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">return</span> <span class="p">(</span>
<span class="p"><</span><span class="nt">div</span> <span class="na">className</span><span class="p">=</span><span class="s">"eventContainer"</span><span class="p">></span>
<span class="p"><</span><span class="nt">h2</span><span class="p">></span> ... <span class="p"></</span><span class="nt">h2</span><span class="p">></span>
<span class="p"><</span><span class="nt">ul</span><span class="p">></span> ... <span class="p"></</span><span class="nt">ul</span><span class="p">></span>
<span class="p"></</span><span class="nt">div</span><span class="p">></span>
<span class="p">);</span>
</code></pre></div></div>
<p>Open <code class="language-plaintext highlighter-rouge">app/views/layouts/application.html.erb</code> and add a couple of custom fonts:</p>
<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt"><head></span>
<span class="nt"><title></span>EventManager<span class="nt"></title></span>
<span class="nt"><meta</span> <span class="na">name=</span><span class="s">"viewport"</span> <span class="na">content=</span><span class="s">"width=device-width,initial-scale=1"</span><span class="nt">></span>
<span class="nt"><</span><span class="err">%=</span> <span class="na">csrf_meta_tags</span> <span class="err">%</span><span class="nt">></span>
<span class="nt"><</span><span class="err">%=</span> <span class="na">csp_meta_tag</span> <span class="err">%</span><span class="nt">></span>
<span class="nt"><link</span> <span class="na">rel=</span><span class="s">"preconnect"</span> <span class="na">href=</span><span class="s">"https://fonts.googleapis.com"</span><span class="nt">></span>
<span class="nt"><link</span> <span class="na">rel=</span><span class="s">"preconnect"</span> <span class="na">href=</span><span class="s">"https://fonts.gstatic.com"</span> <span class="na">crossorigin</span><span class="nt">></span>
<span class="nt"><link</span> <span class="na">href=</span><span class="s">"https://fonts.googleapis.com/css2?family=Maven+Pro:wght@400;500;700&family=Roboto:ital,wght@0,300;0,400;0,700;1,400&display=swap"</span> <span class="na">rel=</span><span class="s">"stylesheet"</span><span class="nt">></span>
...
<span class="nt"></head></span>
</code></pre></div></div>
<h3 id="bundler-specific-configuration">Bundler Specific Configuration</h3>
<p>Depending on the bundler you are using, some extra configuration is necessary.</p>
<h4 id="esbuild-1">esbuild</h4>
<p>If you are using esbuild, you need to delete <code class="language-plaintext highlighter-rouge">app/assets/stylesheets/application.css</code> from the project, as otherwise the next time you start the Rails server you will see the following warning:</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ActionView::Template::Error <span class="o">(</span>Multiple files with the same output path cannot be linked <span class="o">(</span>“application.css”<span class="o">)</span>
</code></pre></div></div>
<p>You could alternatively choose to not create an <code class="language-plaintext highlighter-rouge">App.css</code> file and add all of the CSS to <code class="language-plaintext highlighter-rouge">app/assets/stylesheets.css</code> for Rails’ asset pipeline to handle. It doesn’t make a big difference in the case of this tutorial, but I for your average React app, it makes more sense for the CSS to live alongside the components it is styling.</p>
<h4 id="shakapacker-1">Shakapacker</h4>
<p>If you are using Shakapacker, you’ll need to install some more packages, so that it can handle CSS files:</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>yarn add css-loader style-loader mini-css-extract-plugin css-minimizer-webpack-plugin
</code></pre></div></div>
<p>Then restart the server for good measure.</p>
<p>See the Shakapacker docs for more details: <a href="https://github.com/shakacode/shakapacker#css">https://github.com/shakacode/shakapacker#css</a></p>
<p>Either way, now everything should be styled nicely.</p>
<p><img src="https://res.cloudinary.com/hibbard/image/upload/v1648131942/event-manager-hooks/07-event-manager-styled.png" alt="A nicely styled Event Manager" /></p>
<h3 id="highlighting-the-selected-event">Highlighting the Selected Event</h3>
<p>Before we move on, let’s make one final tweak and add some styling to the selected event in the list of events. This makes it easier for the user to see which event they are currently viewing at a glance.</p>
<p>All we have to do here is to swap the <code class="language-plaintext highlighter-rouge"><Link></code> component, for a <a href="https://reactrouterdotcom.fly.dev/docs/en/v6/api#navlink"><NavLink> component</a> in the <code class="language-plaintext highlighter-rouge"><EventList></code> component.</p>
<div class="language-jsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">Link</span><span class="p">,</span> <span class="nx">NavLink</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">react-router-dom</span><span class="dl">'</span><span class="p">;</span>
<span class="kd">const</span> <span class="nx">EventList</span> <span class="o">=</span> <span class="p">({</span> <span class="nx">events</span> <span class="p">})</span> <span class="o">=></span> <span class="p">{</span>
<span class="kd">const</span> <span class="nx">renderEvents</span> <span class="o">=</span> <span class="p">(</span><span class="nx">eventArray</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">eventArray</span><span class="p">.</span><span class="nx">sort</span><span class="p">((</span><span class="nx">a</span><span class="p">,</span> <span class="nx">b</span><span class="p">)</span> <span class="o">=></span> <span class="k">new</span> <span class="nb">Date</span><span class="p">(</span><span class="nx">b</span><span class="p">.</span><span class="nx">event_date</span><span class="p">)</span> <span class="o">-</span> <span class="k">new</span> <span class="nb">Date</span><span class="p">(</span><span class="nx">a</span><span class="p">.</span><span class="nx">event_date</span><span class="p">));</span>
<span class="k">return</span> <span class="nx">eventArray</span><span class="p">.</span><span class="nx">map</span><span class="p">((</span><span class="nx">event</span><span class="p">)</span> <span class="o">=></span> <span class="p">(</span>
<span class="p"><</span><span class="nt">li</span> <span class="na">key</span><span class="p">=</span><span class="si">{</span><span class="nx">event</span><span class="p">.</span><span class="nx">id</span><span class="si">}</span><span class="p">></span>
<span class="p"><</span><span class="nc">NavLink</span> <span class="na">to</span><span class="p">=</span><span class="si">{</span><span class="s2">`/events/</span><span class="p">${</span><span class="nx">event</span><span class="p">.</span><span class="nx">id</span><span class="p">}</span><span class="s2">`</span><span class="si">}</span><span class="p">></span>
<span class="si">{</span><span class="nx">event</span><span class="p">.</span><span class="nx">event_date</span><span class="si">}</span>
<span class="si">{</span><span class="dl">'</span><span class="s1"> - </span><span class="dl">'</span><span class="si">}</span>
<span class="si">{</span><span class="nx">event</span><span class="p">.</span><span class="nx">event_type</span><span class="si">}</span>
<span class="p"></</span><span class="nc">NavLink</span><span class="p">></span>
<span class="p"></</span><span class="nt">li</span><span class="p">></span>
<span class="p">));</span>
<span class="p">};</span>
<span class="p">...</span>
<span class="p">};</span>
</code></pre></div></div>
<p>A <code class="language-plaintext highlighter-rouge"><NavLink></code> is a is a special kind of <code class="language-plaintext highlighter-rouge"><Link></code> that will add an “active” class to the currently active link, which we can now target through our CSS</p>
<h2 id="creating-an-event">Creating an Event</h2>
<p>So far we have the <em>Read</em> functionality of our CRUD app. Now let’s add the ability to create an event.</p>
<p>Start off in the <code class="language-plaintext highlighter-rouge"><Editor></code> component:</p>
<div class="language-jsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">...</span>
<span class="k">import</span> <span class="nx">EventForm</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">./EventForm</span><span class="dl">'</span><span class="p">;</span>
<span class="kd">const</span> <span class="nx">Editor</span> <span class="o">=</span> <span class="p">()</span> <span class="o">=></span> <span class="p">{</span>
<span class="p">...</span>
<span class="k">return</span> <span class="p">(</span>
<span class="p"><></span>
<span class="p"><</span><span class="nc">Header</span> <span class="p">/></span>
<span class="p"><</span><span class="nt">div</span> <span class="na">className</span><span class="p">=</span><span class="s">"grid"</span><span class="p">></span>
<span class="si">{</span><span class="nx">isError</span> <span class="o">&&</span> <span class="p"><</span><span class="nt">p</span><span class="p">></span>Something went wrong. Check the console.<span class="p"></</span><span class="nt">p</span><span class="p">></span><span class="si">}</span>
<span class="si">{</span><span class="nx">isLoading</span> <span class="p">?</span> <span class="p">(</span>
<span class="p"><</span><span class="nt">p</span><span class="p">></span>Loading...<span class="p"></</span><span class="nt">p</span><span class="p">></span>
<span class="p">)</span> <span class="p">:</span> <span class="p">(</span>
<span class="p"><></span>
<span class="p"><</span><span class="nc">EventList</span> <span class="na">events</span><span class="p">=</span><span class="si">{</span><span class="nx">events</span><span class="si">}</span> <span class="p">/></span>
<span class="p"><</span><span class="nc">Routes</span><span class="p">></span>
<span class="p"><</span><span class="nc">Route</span> <span class="na">path</span><span class="p">=</span><span class="s">"new"</span> <span class="na">element</span><span class="p">=</span><span class="si">{</span><span class="p"><</span><span class="nc">EventForm</span> <span class="p">/></span><span class="si">}</span> <span class="p">/></span>
<span class="p"><</span><span class="nc">Route</span> <span class="na">path</span><span class="p">=</span><span class="s">":id"</span> <span class="na">element</span><span class="p">=</span><span class="si">{</span><span class="p"><</span><span class="nc">Event</span> <span class="na">events</span><span class="p">=</span><span class="si">{</span><span class="nx">events</span><span class="si">}</span> <span class="p">/></span><span class="si">}</span> <span class="p">/></span>
<span class="p"></</span><span class="nc">Routes</span><span class="p">></span>
<span class="p"></></span>
<span class="p">)</span><span class="si">}</span>
<span class="p"></</span><span class="nt">div</span><span class="p">></span>
<span class="p"></></span>
<span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>
<p>Here, we’ve added a new <code class="language-plaintext highlighter-rouge"><Route></code> component, whose <code class="language-plaintext highlighter-rouge">path</code> property is set to “new”. When this matches the current URL (i.e. <code class="language-plaintext highlighter-rouge">/events/new</code>), it will render an <code class="language-plaintext highlighter-rouge"><EventForm></code> component, which will contain our form for adding (and later editing) events.</p>
<p>Next, let’s add a link to display the form in the <code class="language-plaintext highlighter-rouge"><EventList></code> component.</p>
<div class="language-jsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">EventList</span> <span class="o">=</span> <span class="p">({</span> <span class="nx">events</span> <span class="p">})</span> <span class="o">=></span> <span class="p">{</span>
<span class="kd">const</span> <span class="nx">renderEvents</span> <span class="o">=</span> <span class="p">(</span><span class="nx">eventArray</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span> <span class="p">...</span> <span class="p">};</span>
<span class="p">...</span>
<span class="k">return</span> <span class="p">(</span>
<span class="p"><</span><span class="nt">section</span> <span class="na">className</span><span class="p">=</span><span class="s">"eventList"</span><span class="p">></span>
<span class="p"><</span><span class="nt">h2</span><span class="p">></span>
Events
<span class="p"><</span><span class="nc">Link</span> <span class="na">to</span><span class="p">=</span><span class="s">"/events/new"</span><span class="p">></span>New Event<span class="p"></</span><span class="nc">Link</span><span class="p">></span>
<span class="p"></</span><span class="nt">h2</span><span class="p">></span>
<span class="p"><</span><span class="nt">ul</span><span class="p">></span><span class="si">{</span><span class="nx">renderEvents</span><span class="p">(</span><span class="nx">events</span><span class="p">)</span><span class="si">}</span><span class="p"></</span><span class="nt">ul</span><span class="p">></span>
<span class="p"></</span><span class="nt">section</span><span class="p">></span>
<span class="p">);</span>
<span class="p">};</span>
</code></pre></div></div>
<p>Now, let’s create the <code class="language-plaintext highlighter-rouge"><EventForm></code> component:</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">touch </span>app/javascript/components/EventForm.js
</code></pre></div></div>
<p>And add the following content:</p>
<div class="language-jsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="nx">React</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">react</span><span class="dl">'</span><span class="p">;</span>
<span class="kd">const</span> <span class="nx">EventForm</span> <span class="o">=</span> <span class="p">()</span> <span class="o">=></span> <span class="p">{</span>
<span class="kd">const</span> <span class="nx">handleSubmit</span> <span class="o">=</span> <span class="p">(</span><span class="nx">e</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">e</span><span class="p">.</span><span class="nx">preventDefault</span><span class="p">();</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="dl">'</span><span class="s1">Submitted</span><span class="dl">'</span><span class="p">);</span>
<span class="p">};</span>
<span class="k">return</span> <span class="p">(</span>
<span class="p"><</span><span class="nt">section</span><span class="p">></span>
<span class="p"><</span><span class="nt">h2</span><span class="p">></span>New Event<span class="p"></</span><span class="nt">h2</span><span class="p">></span>
<span class="p"><</span><span class="nt">form</span> <span class="na">className</span><span class="p">=</span><span class="s">"eventForm"</span> <span class="na">onSubmit</span><span class="p">=</span><span class="si">{</span><span class="nx">handleSubmit</span><span class="si">}</span><span class="p">></span>
<span class="p"><</span><span class="nt">div</span><span class="p">></span>
<span class="p"><</span><span class="nt">label</span> <span class="na">htmlFor</span><span class="p">=</span><span class="s">"event_type"</span><span class="p">></span>
<span class="p"><</span><span class="nt">strong</span><span class="p">></span>Type:<span class="p"></</span><span class="nt">strong</span><span class="p">></span>
<span class="p"><</span><span class="nt">input</span> <span class="na">type</span><span class="p">=</span><span class="s">"text"</span> <span class="na">id</span><span class="p">=</span><span class="s">"event_type"</span> <span class="na">name</span><span class="p">=</span><span class="s">"event_type"</span> <span class="p">/></span>
<span class="p"></</span><span class="nt">label</span><span class="p">></span>
<span class="p"></</span><span class="nt">div</span><span class="p">></span>
<span class="p"><</span><span class="nt">div</span><span class="p">></span>
<span class="p"><</span><span class="nt">label</span> <span class="na">htmlFor</span><span class="p">=</span><span class="s">"event_date"</span><span class="p">></span>
<span class="p"><</span><span class="nt">strong</span><span class="p">></span>Date:<span class="p"></</span><span class="nt">strong</span><span class="p">></span>
<span class="p"><</span><span class="nt">input</span> <span class="na">type</span><span class="p">=</span><span class="s">"text"</span> <span class="na">id</span><span class="p">=</span><span class="s">"event_date"</span> <span class="na">name</span><span class="p">=</span><span class="s">"event_date"</span> <span class="p">/></span>
<span class="p"></</span><span class="nt">label</span><span class="p">></span>
<span class="p"></</span><span class="nt">div</span><span class="p">></span>
<span class="p"><</span><span class="nt">div</span><span class="p">></span>
<span class="p"><</span><span class="nt">label</span> <span class="na">htmlFor</span><span class="p">=</span><span class="s">"title"</span><span class="p">></span>
<span class="p"><</span><span class="nt">strong</span><span class="p">></span>Title:<span class="p"></</span><span class="nt">strong</span><span class="p">></span>
<span class="p"><</span><span class="nt">textarea</span> <span class="na">cols</span><span class="p">=</span><span class="s">"30"</span> <span class="na">rows</span><span class="p">=</span><span class="s">"10"</span> <span class="na">id</span><span class="p">=</span><span class="s">"title"</span> <span class="na">name</span><span class="p">=</span><span class="s">"title"</span> <span class="p">/></span>
<span class="p"></</span><span class="nt">label</span><span class="p">></span>
<span class="p"></</span><span class="nt">div</span><span class="p">></span>
<span class="p"><</span><span class="nt">div</span><span class="p">></span>
<span class="p"><</span><span class="nt">label</span> <span class="na">htmlFor</span><span class="p">=</span><span class="s">"speaker"</span><span class="p">></span>
<span class="p"><</span><span class="nt">strong</span><span class="p">></span>Speakers:<span class="p"></</span><span class="nt">strong</span><span class="p">></span>
<span class="p"><</span><span class="nt">input</span> <span class="na">type</span><span class="p">=</span><span class="s">"text"</span> <span class="na">id</span><span class="p">=</span><span class="s">"speaker"</span> <span class="na">name</span><span class="p">=</span><span class="s">"speaker"</span> <span class="p">/></span>
<span class="p"></</span><span class="nt">label</span><span class="p">></span>
<span class="p"></</span><span class="nt">div</span><span class="p">></span>
<span class="p"><</span><span class="nt">div</span><span class="p">></span>
<span class="p"><</span><span class="nt">label</span> <span class="na">htmlFor</span><span class="p">=</span><span class="s">"host"</span><span class="p">></span>
<span class="p"><</span><span class="nt">strong</span><span class="p">></span>Hosts:<span class="p"></</span><span class="nt">strong</span><span class="p">></span>
<span class="p"><</span><span class="nt">input</span> <span class="na">type</span><span class="p">=</span><span class="s">"text"</span> <span class="na">id</span><span class="p">=</span><span class="s">"host"</span> <span class="na">name</span><span class="p">=</span><span class="s">"host"</span> <span class="p">/></span>
<span class="p"></</span><span class="nt">label</span><span class="p">></span>
<span class="p"></</span><span class="nt">div</span><span class="p">></span>
<span class="p"><</span><span class="nt">div</span><span class="p">></span>
<span class="p"><</span><span class="nt">label</span> <span class="na">htmlFor</span><span class="p">=</span><span class="s">"published"</span><span class="p">></span>
<span class="p"><</span><span class="nt">strong</span><span class="p">></span>Publish:<span class="p"></</span><span class="nt">strong</span><span class="p">></span>
<span class="p"><</span><span class="nt">input</span> <span class="na">type</span><span class="p">=</span><span class="s">"checkbox"</span> <span class="na">id</span><span class="p">=</span><span class="s">"published"</span> <span class="na">name</span><span class="p">=</span><span class="s">"published"</span> <span class="p">/></span>
<span class="p"></</span><span class="nt">label</span><span class="p">></span>
<span class="p"></</span><span class="nt">div</span><span class="p">></span>
<span class="p"><</span><span class="nt">div</span> <span class="na">className</span><span class="p">=</span><span class="s">"form-actions"</span><span class="p">></span>
<span class="p"><</span><span class="nt">button</span> <span class="na">type</span><span class="p">=</span><span class="s">"submit"</span><span class="p">></span>Save<span class="p"></</span><span class="nt">button</span><span class="p">></span>
<span class="p"></</span><span class="nt">div</span><span class="p">></span>
<span class="p"></</span><span class="nt">form</span><span class="p">></span>
<span class="p"></</span><span class="nt">section</span><span class="p">></span>
<span class="p">);</span>
<span class="p">};</span>
<span class="k">export</span> <span class="k">default</span> <span class="nx">EventForm</span><span class="p">;</span>
</code></pre></div></div>
<p>At this point the form should appear and when you click <em>Save</em>, it should log “Submitted” to the console.</p>
<p><img src="https://res.cloudinary.com/hibbard/image/upload/v1648132916/event-manager-hooks/08-new-event-form.png" alt="New event form displaying message 'Submitted' in console" /></p>
<h2 id="form-validation">Form Validation</h2>
<p>Now, let’s add in some validation to make sure all of the fields (apart from <code class="language-plaintext highlighter-rouge">published</code>) are filled out. All of the action will take place in the <code class="language-plaintext highlighter-rouge"><EventForm></code> component.</p>
<p>Change the code in that file, so that it looks like so:</p>
<div class="language-jsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="nx">React</span><span class="p">,</span> <span class="p">{</span> <span class="nx">useState</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">react</span><span class="dl">'</span><span class="p">;</span>
<span class="kd">const</span> <span class="nx">EventForm</span> <span class="o">=</span> <span class="p">()</span> <span class="o">=></span> <span class="p">{</span>
<span class="kd">const</span> <span class="p">[</span><span class="nx">event</span><span class="p">,</span> <span class="nx">setEvent</span><span class="p">]</span> <span class="o">=</span> <span class="nx">useState</span><span class="p">({</span>
<span class="na">event_type</span><span class="p">:</span> <span class="dl">''</span><span class="p">,</span>
<span class="na">event_date</span><span class="p">:</span> <span class="dl">''</span><span class="p">,</span>
<span class="na">title</span><span class="p">:</span> <span class="dl">''</span><span class="p">,</span>
<span class="na">speaker</span><span class="p">:</span> <span class="dl">''</span><span class="p">,</span>
<span class="na">host</span><span class="p">:</span> <span class="dl">''</span><span class="p">,</span>
<span class="na">published</span><span class="p">:</span> <span class="kc">false</span><span class="p">,</span>
<span class="p">});</span>
<span class="kd">const</span> <span class="p">[</span><span class="nx">formErrors</span><span class="p">,</span> <span class="nx">setFormErrors</span><span class="p">]</span> <span class="o">=</span> <span class="nx">useState</span><span class="p">({});</span>
<span class="kd">const</span> <span class="nx">handleInputChange</span> <span class="o">=</span> <span class="p">(</span><span class="nx">e</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="kd">const</span> <span class="p">{</span> <span class="nx">target</span> <span class="p">}</span> <span class="o">=</span> <span class="nx">e</span><span class="p">;</span>
<span class="kd">const</span> <span class="p">{</span> <span class="nx">name</span> <span class="p">}</span> <span class="o">=</span> <span class="nx">target</span><span class="p">;</span>
<span class="kd">const</span> <span class="nx">value</span> <span class="o">=</span> <span class="nx">target</span><span class="p">.</span><span class="nx">type</span> <span class="o">===</span> <span class="dl">'</span><span class="s1">checkbox</span><span class="dl">'</span> <span class="p">?</span> <span class="nx">target</span><span class="p">.</span><span class="nx">checked</span> <span class="p">:</span> <span class="nx">target</span><span class="p">.</span><span class="nx">value</span><span class="p">;</span>
<span class="nx">setEvent</span><span class="p">({</span> <span class="p">...</span><span class="nx">event</span><span class="p">,</span> <span class="p">[</span><span class="nx">name</span><span class="p">]:</span> <span class="nx">value</span> <span class="p">});</span>
<span class="p">};</span>
<span class="kd">const</span> <span class="nx">validateEvent</span> <span class="o">=</span> <span class="p">()</span> <span class="o">=></span> <span class="p">{</span>
<span class="kd">const</span> <span class="nx">errors</span> <span class="o">=</span> <span class="p">{};</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">event</span><span class="p">.</span><span class="nx">event_type</span> <span class="o">===</span> <span class="dl">''</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">errors</span><span class="p">.</span><span class="nx">event_type</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">You must enter an event type</span><span class="dl">'</span><span class="p">;</span>
<span class="p">}</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">event</span><span class="p">.</span><span class="nx">event_date</span> <span class="o">===</span> <span class="dl">''</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">errors</span><span class="p">.</span><span class="nx">event_date</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">You must enter a valid date</span><span class="dl">'</span><span class="p">;</span>
<span class="p">}</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">event</span><span class="p">.</span><span class="nx">title</span> <span class="o">===</span> <span class="dl">''</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">errors</span><span class="p">.</span><span class="nx">title</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">You must enter a title</span><span class="dl">'</span><span class="p">;</span>
<span class="p">}</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">event</span><span class="p">.</span><span class="nx">speaker</span> <span class="o">===</span> <span class="dl">''</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">errors</span><span class="p">.</span><span class="nx">speaker</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">You must enter at least one speaker</span><span class="dl">'</span><span class="p">;</span>
<span class="p">}</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">event</span><span class="p">.</span><span class="nx">host</span> <span class="o">===</span> <span class="dl">''</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">errors</span><span class="p">.</span><span class="nx">host</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">You must enter at least one host</span><span class="dl">'</span><span class="p">;</span>
<span class="p">}</span>
<span class="k">return</span> <span class="nx">errors</span><span class="p">;</span>
<span class="p">};</span>
<span class="kd">const</span> <span class="nx">isEmptyObject</span> <span class="o">=</span> <span class="p">(</span><span class="nx">obj</span><span class="p">)</span> <span class="o">=></span> <span class="nb">Object</span><span class="p">.</span><span class="nx">keys</span><span class="p">(</span><span class="nx">obj</span><span class="p">).</span><span class="nx">length</span> <span class="o">===</span> <span class="mi">0</span><span class="p">;</span>
<span class="kd">const</span> <span class="nx">renderErrors</span> <span class="o">=</span> <span class="p">()</span> <span class="o">=></span> <span class="p">{</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">isEmptyObject</span><span class="p">(</span><span class="nx">formErrors</span><span class="p">))</span> <span class="p">{</span>
<span class="k">return</span> <span class="kc">null</span><span class="p">;</span>
<span class="p">}</span>
<span class="k">return</span> <span class="p">(</span>
<span class="p"><</span><span class="nt">div</span> <span class="na">className</span><span class="p">=</span><span class="s">"errors"</span><span class="p">></span>
<span class="p"><</span><span class="nt">h3</span><span class="p">></span>The following errors prohibited the event from being saved:<span class="p"></</span><span class="nt">h3</span><span class="p">></span>
<span class="p"><</span><span class="nt">ul</span><span class="p">></span>
<span class="si">{</span><span class="nb">Object</span><span class="p">.</span><span class="nx">values</span><span class="p">(</span><span class="nx">formErrors</span><span class="p">).</span><span class="nx">map</span><span class="p">((</span><span class="nx">formError</span><span class="p">)</span> <span class="o">=></span> <span class="p">(</span>
<span class="p"><</span><span class="nt">li</span> <span class="na">key</span><span class="p">=</span><span class="si">{</span><span class="nx">formError</span><span class="si">}</span><span class="p">></span><span class="si">{</span><span class="nx">formError</span><span class="si">}</span><span class="p"></</span><span class="nt">li</span><span class="p">></span>
<span class="p">))</span><span class="si">}</span>
<span class="p"></</span><span class="nt">ul</span><span class="p">></span>
<span class="p"></</span><span class="nt">div</span><span class="p">></span>
<span class="p">);</span>
<span class="p">};</span>
<span class="kd">const</span> <span class="nx">handleSubmit</span> <span class="o">=</span> <span class="p">(</span><span class="nx">e</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">e</span><span class="p">.</span><span class="nx">preventDefault</span><span class="p">();</span>
<span class="kd">const</span> <span class="nx">errors</span> <span class="o">=</span> <span class="nx">validateEvent</span><span class="p">(</span><span class="nx">event</span><span class="p">);</span>
<span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">isEmptyObject</span><span class="p">(</span><span class="nx">errors</span><span class="p">))</span> <span class="p">{</span>
<span class="nx">setFormErrors</span><span class="p">(</span><span class="nx">errors</span><span class="p">);</span>
<span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">event</span><span class="p">);</span>
<span class="p">}</span>
<span class="p">};</span>
<span class="k">return</span> <span class="p">(</span>
<span class="p"><</span><span class="nt">section</span><span class="p">></span>
<span class="si">{</span><span class="nx">renderErrors</span><span class="p">()</span><span class="si">}</span>
<span class="p"><</span><span class="nt">h2</span><span class="p">></span>New Event<span class="p"></</span><span class="nt">h2</span><span class="p">></span>
<span class="p"><</span><span class="nt">form</span> <span class="na">className</span><span class="p">=</span><span class="s">"eventForm"</span> <span class="na">onSubmit</span><span class="p">=</span><span class="si">{</span><span class="nx">handleSubmit</span><span class="si">}</span><span class="p">></span>
<span class="p"><</span><span class="nt">div</span><span class="p">></span>
<span class="p"><</span><span class="nt">label</span> <span class="na">htmlFor</span><span class="p">=</span><span class="s">"event_type"</span><span class="p">></span>
<span class="p"><</span><span class="nt">strong</span><span class="p">></span>Type:<span class="p"></</span><span class="nt">strong</span><span class="p">></span>
<span class="p"><</span><span class="nt">input</span>
<span class="na">type</span><span class="p">=</span><span class="s">"text"</span>
<span class="na">id</span><span class="p">=</span><span class="s">"event_type"</span>
<span class="na">name</span><span class="p">=</span><span class="s">"event_type"</span>
<span class="na">onChange</span><span class="p">=</span><span class="si">{</span><span class="nx">handleInputChange</span><span class="si">}</span>
<span class="p">/></span>
<span class="p"></</span><span class="nt">label</span><span class="p">></span>
<span class="p"></</span><span class="nt">div</span><span class="p">></span>
<span class="p"><</span><span class="nt">div</span><span class="p">></span>
<span class="p"><</span><span class="nt">label</span> <span class="na">htmlFor</span><span class="p">=</span><span class="s">"event_date"</span><span class="p">></span>
<span class="p"><</span><span class="nt">strong</span><span class="p">></span>Date:<span class="p"></</span><span class="nt">strong</span><span class="p">></span>
<span class="p"><</span><span class="nt">input</span>
<span class="na">type</span><span class="p">=</span><span class="s">"text"</span>
<span class="na">id</span><span class="p">=</span><span class="s">"event_date"</span>
<span class="na">name</span><span class="p">=</span><span class="s">"event_date"</span>
<span class="na">onChange</span><span class="p">=</span><span class="si">{</span><span class="nx">handleInputChange</span><span class="si">}</span>
<span class="p">/></span>
<span class="p"></</span><span class="nt">label</span><span class="p">></span>
<span class="p"></</span><span class="nt">div</span><span class="p">></span>
<span class="p"><</span><span class="nt">div</span><span class="p">></span>
<span class="p"><</span><span class="nt">label</span> <span class="na">htmlFor</span><span class="p">=</span><span class="s">"title"</span><span class="p">></span>
<span class="p"><</span><span class="nt">strong</span><span class="p">></span>Title:<span class="p"></</span><span class="nt">strong</span><span class="p">></span>
<span class="p"><</span><span class="nt">textarea</span>
<span class="na">cols</span><span class="p">=</span><span class="s">"30"</span>
<span class="na">rows</span><span class="p">=</span><span class="s">"10"</span>
<span class="na">id</span><span class="p">=</span><span class="s">"title"</span>
<span class="na">name</span><span class="p">=</span><span class="s">"title"</span>
<span class="na">onChange</span><span class="p">=</span><span class="si">{</span><span class="nx">handleInputChange</span><span class="si">}</span>
<span class="p">/></span>
<span class="p"></</span><span class="nt">label</span><span class="p">></span>
<span class="p"></</span><span class="nt">div</span><span class="p">></span>
<span class="p"><</span><span class="nt">div</span><span class="p">></span>
<span class="p"><</span><span class="nt">label</span> <span class="na">htmlFor</span><span class="p">=</span><span class="s">"speaker"</span><span class="p">></span>
<span class="p"><</span><span class="nt">strong</span><span class="p">></span>Speakers:<span class="p"></</span><span class="nt">strong</span><span class="p">></span>
<span class="p"><</span><span class="nt">input</span>
<span class="na">type</span><span class="p">=</span><span class="s">"text"</span>
<span class="na">id</span><span class="p">=</span><span class="s">"speaker"</span>
<span class="na">name</span><span class="p">=</span><span class="s">"speaker"</span>
<span class="na">onChange</span><span class="p">=</span><span class="si">{</span><span class="nx">handleInputChange</span><span class="si">}</span>
<span class="p">/></span>
<span class="p"></</span><span class="nt">label</span><span class="p">></span>
<span class="p"></</span><span class="nt">div</span><span class="p">></span>
<span class="p"><</span><span class="nt">div</span><span class="p">></span>
<span class="p"><</span><span class="nt">label</span> <span class="na">htmlFor</span><span class="p">=</span><span class="s">"host"</span><span class="p">></span>
<span class="p"><</span><span class="nt">strong</span><span class="p">></span>Hosts:<span class="p"></</span><span class="nt">strong</span><span class="p">></span>
<span class="p"><</span><span class="nt">input</span>
<span class="na">type</span><span class="p">=</span><span class="s">"text"</span>
<span class="na">id</span><span class="p">=</span><span class="s">"host"</span>
<span class="na">name</span><span class="p">=</span><span class="s">"host"</span>
<span class="na">onChange</span><span class="p">=</span><span class="si">{</span><span class="nx">handleInputChange</span><span class="si">}</span>
<span class="p">/></span>
<span class="p"></</span><span class="nt">label</span><span class="p">></span>
<span class="p"></</span><span class="nt">div</span><span class="p">></span>
<span class="p"><</span><span class="nt">div</span><span class="p">></span>
<span class="p"><</span><span class="nt">label</span> <span class="na">htmlFor</span><span class="p">=</span><span class="s">"published"</span><span class="p">></span>
<span class="p"><</span><span class="nt">strong</span><span class="p">></span>Publish:<span class="p"></</span><span class="nt">strong</span><span class="p">></span>
<span class="p"><</span><span class="nt">input</span>
<span class="na">type</span><span class="p">=</span><span class="s">"checkbox"</span>
<span class="na">id</span><span class="p">=</span><span class="s">"published"</span>
<span class="na">name</span><span class="p">=</span><span class="s">"published"</span>
<span class="na">onChange</span><span class="p">=</span><span class="si">{</span><span class="nx">handleInputChange</span><span class="si">}</span>
<span class="p">/></span>
<span class="p"></</span><span class="nt">label</span><span class="p">></span>
<span class="p"></</span><span class="nt">div</span><span class="p">></span>
<span class="p"><</span><span class="nt">div</span> <span class="na">className</span><span class="p">=</span><span class="s">"form-actions"</span><span class="p">></span>
<span class="p"><</span><span class="nt">button</span> <span class="na">type</span><span class="p">=</span><span class="s">"submit"</span><span class="p">></span>Save<span class="p"></</span><span class="nt">button</span><span class="p">></span>
<span class="p"></</span><span class="nt">div</span><span class="p">></span>
<span class="p"></</span><span class="nt">form</span><span class="p">></span>
<span class="p"></</span><span class="nt">section</span><span class="p">></span>
<span class="p">);</span>
<span class="p">};</span>
<span class="k">export</span> <span class="k">default</span> <span class="nx">EventForm</span><span class="p">;</span>
</code></pre></div></div>
<p>We start off by defining two variables in state: <code class="language-plaintext highlighter-rouge">event</code> and <code class="language-plaintext highlighter-rouge">formErrors</code>. The <code class="language-plaintext highlighter-rouge">event</code> variable is initialized as an object with some sensible defaults and <code class="language-plaintext highlighter-rouge">formErrors</code> is initialized as an empty object.</p>
<p>Next comes a <code class="language-plaintext highlighter-rouge">handleInputChange</code> function. We are going to make all of the fields in our form <a href="https://www.sitepoint.com/work-with-forms-in-react/#controlledinputs">controlled inputs</a>, which is to say React will responsible for maintaining and setting their state. The <code class="language-plaintext highlighter-rouge">handleInputChange</code> function will be called whenever the user changes the values of any of the fields and it will update the <code class="language-plaintext highlighter-rouge">event</code> object, so that it mirrors what has been entered into the form. Be aware of the square bracket notation that allows us to use a variable (<code class="language-plaintext highlighter-rouge">name</code>) as an object key.</p>
<p>After that we have a couple of helper functions: <code class="language-plaintext highlighter-rouge">validateEvent</code> and <code class="language-plaintext highlighter-rouge">isEmptyObject</code>. The first of these runs a bunch of checks on the <code class="language-plaintext highlighter-rouge">event</code> object and returns an object containing any errors, whereas the second returns <code class="language-plaintext highlighter-rouge">true</code> or <code class="language-plaintext highlighter-rouge">false</code> depending on whether the object it is passed has any properties or not.</p>
<p>We then have a <code class="language-plaintext highlighter-rouge">renderErrors</code> function which returns <code class="language-plaintext highlighter-rouge">null</code> if the <code class="language-plaintext highlighter-rouge">formErrors</code> object is empty, or otherwise some JSX representing a warning that the form could not be saved, as well as a list of errors.</p>
<p>And finally, we have updated our <code class="language-plaintext highlighter-rouge">handleSubmit</code> function to validate the user’s input (and check that each field has a value) and either display an error message if anything is missing, or to log the valid event to the console. We have also updated the JSX slightly and added an <code class="language-plaintext highlighter-rouge">onChange</code> property to all our form inputs.</p>
<h3 id="creating-some-helper-functions">Creating Some Helper Functions</h3>
<p>By now our <code class="language-plaintext highlighter-rouge"><EventForm></code> component is growing pretty large and it would be a good idea to split the more generic functions into a file of their own. Initially <code class="language-plaintext highlighter-rouge">validateEvent</code> and <code class="language-plaintext highlighter-rouge">isEmptyObject</code> are good candidates, as these could conceivably be used elsewhere in the app. This approach will also make the helper functions easier to test, as they are decoupled from React.</p>
<p>Let’s create a new file for these functions:</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">mkdir </span>app/javascript/helpers
<span class="nb">touch </span>app/javascript/helpers/helpers.js
</code></pre></div></div>
<p>Now add the following code to <code class="language-plaintext highlighter-rouge">helpers.js</code>, making sure to remove <code class="language-plaintext highlighter-rouge">validateEvent</code> and <code class="language-plaintext highlighter-rouge">isEmptyObject</code> from the <code class="language-plaintext highlighter-rouge"><EventForm></code> component:</p>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">export</span> <span class="kd">const</span> <span class="nx">isEmptyObject</span> <span class="o">=</span> <span class="nx">obj</span> <span class="o">=></span> <span class="nb">Object</span><span class="p">.</span><span class="nx">keys</span><span class="p">(</span><span class="nx">obj</span><span class="p">).</span><span class="nx">length</span> <span class="o">===</span> <span class="mi">0</span><span class="p">;</span>
<span class="k">export</span> <span class="kd">const</span> <span class="nx">validateEvent</span> <span class="o">=</span> <span class="p">(</span><span class="nx">event</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="kd">const</span> <span class="nx">errors</span> <span class="o">=</span> <span class="p">{};</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">event</span><span class="p">.</span><span class="nx">event_type</span> <span class="o">===</span> <span class="dl">''</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">errors</span><span class="p">.</span><span class="nx">event_type</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">You must enter an event type</span><span class="dl">'</span><span class="p">;</span>
<span class="p">}</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">event</span><span class="p">.</span><span class="nx">event_date</span> <span class="o">===</span> <span class="dl">''</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">errors</span><span class="p">.</span><span class="nx">event_date</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">You must enter a valid date</span><span class="dl">'</span><span class="p">;</span>
<span class="p">}</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">event</span><span class="p">.</span><span class="nx">title</span> <span class="o">===</span> <span class="dl">''</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">errors</span><span class="p">.</span><span class="nx">title</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">You must enter a title</span><span class="dl">'</span><span class="p">;</span>
<span class="p">}</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">event</span><span class="p">.</span><span class="nx">speaker</span> <span class="o">===</span> <span class="dl">''</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">errors</span><span class="p">.</span><span class="nx">speaker</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">You must enter at least one speaker</span><span class="dl">'</span><span class="p">;</span>
<span class="p">}</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">event</span><span class="p">.</span><span class="nx">host</span> <span class="o">===</span> <span class="dl">''</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">errors</span><span class="p">.</span><span class="nx">host</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">You must enter at least one host</span><span class="dl">'</span><span class="p">;</span>
<span class="p">}</span>
<span class="k">return</span> <span class="nx">errors</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>
<p>And import them into the <code class="language-plaintext highlighter-rouge"><EventForm></code> component like so:</p>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">isEmptyObject</span><span class="p">,</span> <span class="nx">validateEvent</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">../helpers/helpers</span><span class="dl">'</span><span class="p">;</span>
</code></pre></div></div>
<p>Now when you attempt to submit a form which is not properly filled out, you should see some nicely formatted errors.</p>
<p><img src="https://res.cloudinary.com/hibbard/image/upload/v1648140575/event-manager-hooks/09-form-validation-error.png" alt="Event Manager - Form submission error" /></p>
<h2 id="making-the-date-field-a-datepicker">Making the Date Field a Datepicker</h2>
<p>The next thing to do is to wire up our date field as a datepicker. For this we’ll use <a href="https://www.npmjs.com/package/pikaday">Pikaday</a>.</p>
<p>First, we need to install the library from npm:</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>npm i pikaday
</code></pre></div></div>
<p>or</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>yarn add pikaday
</code></pre></div></div>
<p>Then in the <code class="language-plaintext highlighter-rouge"><EventForm></code> component, alter the React import, then import Pikaday like so:</p>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="nx">React</span><span class="p">,</span> <span class="p">{</span> <span class="nx">useState</span><span class="p">,</span> <span class="nx">useRef</span><span class="p">,</span> <span class="nx">useEffect</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">react</span><span class="dl">'</span><span class="p">;</span>
<span class="k">import</span> <span class="nx">Pikaday</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">pikaday</span><span class="dl">'</span><span class="p">;</span>
<span class="k">import</span> <span class="dl">'</span><span class="s1">pikaday/css/pikaday.css</span><span class="dl">'</span><span class="p">;</span>
</code></pre></div></div>
<p>At the top of the component add:</p>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">EventForm</span> <span class="o">=</span> <span class="p">()</span> <span class="o">=></span> <span class="p">{</span>
<span class="kd">const</span> <span class="p">[</span><span class="nx">event</span><span class="p">,</span> <span class="nx">setEvent</span><span class="p">]</span> <span class="o">=</span> <span class="nx">useState</span><span class="p">({</span> <span class="p">...</span> <span class="p">});</span>
<span class="kd">const</span> <span class="p">[</span><span class="nx">formErrors</span><span class="p">,</span> <span class="nx">setFormErrors</span><span class="p">]</span> <span class="o">=</span> <span class="nx">useState</span><span class="p">({});</span>
<span class="c1">// new line</span>
<span class="kd">const</span> <span class="nx">dateInput</span> <span class="o">=</span> <span class="nx">useRef</span><span class="p">(</span><span class="kc">null</span><span class="p">);</span>
<span class="p">...</span>
<span class="p">}</span>
</code></pre></div></div>
<p>And change the date field like so:</p>
<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt"><div></span>
<span class="nt"><label</span> <span class="na">htmlFor=</span><span class="s">"event_date"</span><span class="nt">></span>
<span class="nt"><strong></span>Date:<span class="nt"></strong></span>
<span class="nt"><input</span>
<span class="na">type=</span><span class="s">"text"</span>
<span class="na">id=</span><span class="s">"event_date"</span>
<span class="na">name=</span><span class="s">"event_date"</span>
<span class="na">ref=</span><span class="s">{dateInput}</span>
<span class="na">autoComplete=</span><span class="s">"off"</span>
<span class="nt">/></span>
<span class="nt"></label></span>
<span class="nt"></div></span>
</code></pre></div></div>
<p>As you can see, we are using the <a href="https://reactjs.org/docs/hooks-reference.html#useref">useRef hook</a> to create a reference to the date input field, so we can access it elsewhere in the code.</p>
<p>Next, we need to add a <code class="language-plaintext highlighter-rouge">useEffect</code> hook to initialize the datepicker when the component is mounted.</p>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">useEffect</span><span class="p">(()</span> <span class="o">=></span> <span class="p">{</span>
<span class="kd">const</span> <span class="nx">p</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">Pikaday</span><span class="p">({</span>
<span class="na">field</span><span class="p">:</span> <span class="nx">dateInput</span><span class="p">.</span><span class="nx">current</span><span class="p">,</span>
<span class="na">onSelect</span><span class="p">:</span> <span class="p">(</span><span class="nx">date</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="kd">const</span> <span class="nx">formattedDate</span> <span class="o">=</span> <span class="nx">formatDate</span><span class="p">(</span><span class="nx">date</span><span class="p">);</span>
<span class="nx">dateInput</span><span class="p">.</span><span class="nx">current</span><span class="p">.</span><span class="nx">value</span> <span class="o">=</span> <span class="nx">formattedDate</span><span class="p">;</span>
<span class="nx">updateEvent</span><span class="p">(</span><span class="dl">'</span><span class="s1">event_date</span><span class="dl">'</span><span class="p">,</span> <span class="nx">formattedDate</span><span class="p">);</span>
<span class="p">},</span>
<span class="p">});</span>
<span class="c1">// Return a cleanup function.</span>
<span class="c1">// React will call this prior to unmounting.</span>
<span class="k">return</span> <span class="p">()</span> <span class="o">=></span> <span class="nx">p</span><span class="p">.</span><span class="nx">destroy</span><span class="p">();</span>
<span class="p">},</span> <span class="p">[]);</span>
</code></pre></div></div>
<p>Thanks to our ref, the <code class="language-plaintext highlighter-rouge">field</code> property of the configuration object that we are passing to Pikaday’s constructor, points to DOM element we want to turn into a datepicker. The <code class="language-plaintext highlighter-rouge">onSelect</code> method determines what will happen when the user selects a date. In this case, the date is formatted into a YYYY-MM-DD string and the <code class="language-plaintext highlighter-rouge">event</code> object we are holding in state is updated.</p>
<p>We can write the <code class="language-plaintext highlighter-rouge">formatDate</code> function as a helper method in <code class="language-plaintext highlighter-rouge">app/javascript/helpers/helpers.js</code>. This receives a <code class="language-plaintext highlighter-rouge">Date</code> object and returns a YYYY-MM-DD string.</p>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">export</span> <span class="kd">const</span> <span class="nx">formatDate</span> <span class="o">=</span> <span class="p">(</span><span class="nx">d</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="kd">const</span> <span class="nx">YYYY</span> <span class="o">=</span> <span class="nx">d</span><span class="p">.</span><span class="nx">getFullYear</span><span class="p">();</span>
<span class="kd">const</span> <span class="nx">MM</span> <span class="o">=</span> <span class="s2">`0</span><span class="p">${</span><span class="nx">d</span><span class="p">.</span><span class="nx">getMonth</span><span class="p">()</span> <span class="o">+</span> <span class="mi">1</span><span class="p">}</span><span class="s2">`</span><span class="p">.</span><span class="nx">slice</span><span class="p">(</span><span class="o">-</span><span class="mi">2</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">DD</span> <span class="o">=</span> <span class="s2">`0</span><span class="p">${</span><span class="nx">d</span><span class="p">.</span><span class="nx">getDate</span><span class="p">()}</span><span class="s2">`</span><span class="p">.</span><span class="nx">slice</span><span class="p">(</span><span class="o">-</span><span class="mi">2</span><span class="p">);</span>
<span class="k">return</span> <span class="s2">`</span><span class="p">${</span><span class="nx">YYYY</span><span class="p">}</span><span class="s2">-</span><span class="p">${</span><span class="nx">MM</span><span class="p">}</span><span class="s2">-</span><span class="p">${</span><span class="nx">DD</span><span class="p">}</span><span class="s2">`</span><span class="p">;</span>
<span class="p">};</span>
</code></pre></div></div>
<p>Don’t forget to import it in the <code class="language-plaintext highlighter-rouge"><EventForm></code> component:</p>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">formatDate</span><span class="p">,</span> <span class="nx">isEmptyObject</span><span class="p">,</span> <span class="nx">validateEvent</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">../helpers/helpers</span><span class="dl">'</span><span class="p">;</span>
</code></pre></div></div>
<p>We can declare the <code class="language-plaintext highlighter-rouge">updateEvent</code> method in the <code class="language-plaintext highlighter-rouge"><EventForm></code> component:</p>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">updateEvent</span> <span class="o">=</span> <span class="p">(</span><span class="nx">key</span><span class="p">,</span> <span class="nx">value</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">setEvent</span><span class="p">((</span><span class="nx">prevEvent</span><span class="p">)</span> <span class="o">=></span> <span class="p">({</span> <span class="p">...</span><span class="nx">prevEvent</span><span class="p">,</span> <span class="p">[</span><span class="nx">key</span><span class="p">]:</span> <span class="nx">value</span> <span class="p">}));</span>
<span class="p">};</span>
</code></pre></div></div>
<p>Notice that we are calling the <code class="language-plaintext highlighter-rouge">setEvent</code> function slightly differently in that we are passing it a <a href="https://reactjs.org/docs/hooks-reference.html#functional-updates">function as an argument</a>. This callback function receives the previous value of <code class="language-plaintext highlighter-rouge">event</code>, which we spread into a new object, updating the key/value pair that has changed. This object is returned as the new value of <code class="language-plaintext highlighter-rouge">event</code>.</p>
<p>We need to do it like this — and not <code class="language-plaintext highlighter-rouge">setEvent({ ...event, [key]: value })</code> — as otherwise, inside the <code class="language-plaintext highlighter-rouge">onSelect</code> method, <code class="language-plaintext highlighter-rouge">event</code> will point to its initial value (i.e. an empty object). This is because when it is declared, <code class="language-plaintext highlighter-rouge">onSelect</code> forms a closure over <code class="language-plaintext highlighter-rouge">event</code> and captures an incorrect value. You can read more about this here: <a href="https://dmitripavlutin.com/react-hooks-stale-closures/">Be Aware of Stale Closures when Using React Hooks</a>.</p>
<p>Alternatively, we could add <code class="language-plaintext highlighter-rouge">event</code> to the <code class="language-plaintext highlighter-rouge">useEffect</code> dependency array, which would solve the problem. This would however, mean that we are creating a new datepicker every time the user types a character into our form, which is not ideal. There’s more on this in this <a href="https://github.com/facebook/react/issues/14066">GitHub issue</a>.</p>
<p>Finally, let’s update our <code class="language-plaintext highlighter-rouge">handleInputChange</code> function to use this new method:</p>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">handleInputChange</span> <span class="o">=</span> <span class="p">(</span><span class="nx">e</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="kd">const</span> <span class="p">{</span> <span class="nx">target</span> <span class="p">}</span> <span class="o">=</span> <span class="nx">e</span><span class="p">;</span>
<span class="kd">const</span> <span class="p">{</span> <span class="nx">name</span> <span class="p">}</span> <span class="o">=</span> <span class="nx">target</span><span class="p">;</span>
<span class="kd">const</span> <span class="nx">value</span> <span class="o">=</span> <span class="nx">target</span><span class="p">.</span><span class="nx">type</span> <span class="o">===</span> <span class="dl">'</span><span class="s1">checkbox</span><span class="dl">'</span> <span class="p">?</span> <span class="nx">target</span><span class="p">.</span><span class="nx">checked</span> <span class="p">:</span> <span class="nx">target</span><span class="p">.</span><span class="nx">value</span><span class="p">;</span>
<span class="nx">updateEvent</span><span class="p">(</span><span class="nx">name</span><span class="p">,</span> <span class="nx">value</span><span class="p">);</span>
<span class="p">};</span>
</code></pre></div></div>
<p>And that’s it. We now have a datepicker.</p>
<p><img src="https://res.cloudinary.com/hibbard/image/upload/v1648149853/event-manager-hooks/10-pikaday-datepicker-added-to-form.png" alt="New Event form with Pikaday datepicker" /></p>
<h3 id="warning-in-webpack-console">Warning in Webpack Console</h3>
<p>If you are following along using Shakapacker you will see a warning in the console at this point. If you are using esbuild, <a href="#why-pikaday">skip to the next section</a>.</p>
<p>The warning is:</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>WARNING <span class="k">in</span> ./node_modules/pikaday/pikaday.js 15:23-40
Module not found: Error: Can<span class="s1">'t resolve '</span>moment<span class="s1">' ...
</span></code></pre></div></div>
<p>This is caused by the fact that Pikaday has made moment an optional dependency — if it is available, Pikaday requires it, otherwise it doesn’t. Unfortunately, this causes webpack to throw the above error. There is quite a lengthy issue looking at why that is here: https://github.com/webpack/webpack/issues/196.</p>
<p>The Pikaday maintainer doesn’t regard this as problematic and <a href="https://github.com/Pikaday/Pikaday/issues/814#issuecomment-432291461">his advice</a> is to ignore the warning. If we are going to do that, then it would be better to turn off webpack’s full-screen error overlay in <code class="language-plaintext highlighter-rouge">config/webpacker.yml</code>:</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">client</span><span class="pi">:</span>
<span class="c1"># Should we show a full-screen overlay in the browser</span>
<span class="c1"># when there are compiler errors or warnings?</span>
<span class="na">overlay</span><span class="pi">:</span> <span class="no">false</span>
</code></pre></div></div>
<p>Alternatively, you can get rid of the error by commenting out the Moment requires in <code class="language-plaintext highlighter-rouge">node_modules/pikaday/pikaday.js</code>:</p>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">(</span><span class="kd">function</span> <span class="p">(</span><span class="nx">root</span><span class="p">,</span> <span class="nx">factory</span><span class="p">)</span>
<span class="p">{</span>
<span class="dl">'</span><span class="s1">use strict</span><span class="dl">'</span><span class="p">;</span>
<span class="kd">var</span> <span class="nx">moment</span><span class="p">;</span>
<span class="k">if</span> <span class="p">(</span><span class="k">typeof</span> <span class="nx">exports</span> <span class="o">===</span> <span class="dl">'</span><span class="s1">object</span><span class="dl">'</span><span class="p">)</span> <span class="p">{</span>
<span class="c1">// CommonJS module</span>
<span class="c1">// Load moment.js as an optional dependency</span>
<span class="c1">// try { moment = require('moment'); } catch (e) {}</span>
<span class="nx">module</span><span class="p">.</span><span class="nx">exports</span> <span class="o">=</span> <span class="nx">factory</span><span class="p">(</span><span class="nx">moment</span><span class="p">);</span>
<span class="p">}</span> <span class="k">else</span> <span class="k">if</span> <span class="p">(</span><span class="k">typeof</span> <span class="nx">define</span> <span class="o">===</span> <span class="dl">'</span><span class="s1">function</span><span class="dl">'</span> <span class="o">&&</span> <span class="nx">define</span><span class="p">.</span><span class="nx">amd</span><span class="p">)</span> <span class="p">{</span>
<span class="c1">// AMD. Register as an anonymous module.</span>
<span class="nx">define</span><span class="p">(</span><span class="kd">function</span> <span class="p">(</span><span class="nx">req</span><span class="p">)</span>
<span class="p">{</span>
<span class="c1">// Load moment.js as an optional dependency</span>
<span class="kd">var</span> <span class="nx">id</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">moment</span><span class="dl">'</span><span class="p">;</span>
<span class="c1">// try { moment = req(id); } catch (e) {}</span>
<span class="k">return</span> <span class="nx">factory</span><span class="p">(</span><span class="nx">moment</span><span class="p">);</span>
<span class="p">});</span>
<span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
<span class="nx">root</span><span class="p">.</span><span class="nx">Pikaday</span> <span class="o">=</span> <span class="nx">factory</span><span class="p">(</span><span class="nx">root</span><span class="p">.</span><span class="nx">moment</span><span class="p">);</span>
<span class="p">}</span>
<span class="p">}(</span><span class="k">this</span><span class="p">,</span> <span class="kd">function</span> <span class="p">(</span><span class="nx">moment</span><span class="p">)</span>
</code></pre></div></div>
<h3 id="why-pikaday">Why Pikaday?</h3>
<p>So why <em>are</em> we using Pikaday? In addition to the ugly warning Shakapacker users are seeing, there doesn’t seem to be much active development happening on the project. Its <a href="https://github.com/Pikaday/Pikaday">GitHub repo</a> has a lot of open issues, with <a href="https://github.com/Pikaday/Pikaday/issues/862">a</a> <a href="https://github.com/Pikaday/Pikaday/issues/884">couple</a> of them calling for the project to be marked as unmaintained.</p>
<p>Nonetheless, Pikaday is downloaded over 1 million times each month. The current version is stable, used in production and is adequate for our use case. I also wanted to demonstrate how to include a third party library in a React application.</p>
<p>If you would rather use a component-based solution, I recommend <a href="https://www.npmjs.com/package/react-datepicker">React Date Picker</a>. It’s simple to set up and seems to be under active development. Otherwise you could try any of the <a href="https://www.npmjs.com/search?q=react%20date%20picker">other solutions</a> out there.</p>
<p><strong>Ref.:</strong></p>
<ul>
<li><a href="https://reactjs.org/docs/integrating-with-other-libraries.html">https://reactjs.org/docs/integrating-with-other-libraries.html</a></li>
<li><a href="https://stackoverflow.com/questions/30058477/how-can-i-use-pikaday-with-reactjs">https://stackoverflow.com/questions/30058477/how-can-i-use-pikaday-with-reactjs</a></li>
</ul>
<h2 id="saving-an-event">Saving an Event</h2>
<p>Currently, if you save a valid event, the event is logged to the console and nothing else happens. To actually save it to the database, we’re going to pass a callback function to our <code class="language-plaintext highlighter-rouge"><EventForm></code> component, that can be called in the context of its parent.</p>
<p>In the <code class="language-plaintext highlighter-rouge"><Editor></code> component:</p>
<div class="language-jsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">Routes</span><span class="p">,</span> <span class="nx">Route</span><span class="p">,</span> <span class="nx">useNavigate</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">react-router-dom</span><span class="dl">'</span><span class="p">;</span>
<span class="p">...</span>
<span class="kd">const</span> <span class="nx">Editor</span> <span class="o">=</span> <span class="p">()</span> <span class="o">=></span> <span class="p">{</span>
<span class="p">...</span>
<span class="kd">const</span> <span class="nx">navigate</span> <span class="o">=</span> <span class="nx">useNavigate</span><span class="p">();</span>
<span class="nx">useEffect</span><span class="p">(()</span> <span class="o">=></span> <span class="p">{</span> <span class="p">...</span> <span class="p">},</span> <span class="p">[]);</span>
<span class="kd">const</span> <span class="nx">addEvent</span> <span class="o">=</span> <span class="k">async</span> <span class="p">(</span><span class="nx">newEvent</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="k">try</span> <span class="p">{</span>
<span class="kd">const</span> <span class="nx">response</span> <span class="o">=</span> <span class="k">await</span> <span class="nb">window</span><span class="p">.</span><span class="nx">fetch</span><span class="p">(</span><span class="dl">'</span><span class="s1">/api/events</span><span class="dl">'</span><span class="p">,</span> <span class="p">{</span>
<span class="na">method</span><span class="p">:</span> <span class="dl">'</span><span class="s1">POST</span><span class="dl">'</span><span class="p">,</span>
<span class="na">body</span><span class="p">:</span> <span class="nx">JSON</span><span class="p">.</span><span class="nx">stringify</span><span class="p">(</span><span class="nx">newEvent</span><span class="p">),</span>
<span class="na">headers</span><span class="p">:</span> <span class="p">{</span>
<span class="na">Accept</span><span class="p">:</span> <span class="dl">'</span><span class="s1">application/json</span><span class="dl">'</span><span class="p">,</span>
<span class="dl">'</span><span class="s1">Content-Type</span><span class="dl">'</span><span class="p">:</span> <span class="dl">'</span><span class="s1">application/json</span><span class="dl">'</span><span class="p">,</span>
<span class="p">},</span>
<span class="p">});</span>
<span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">response</span><span class="p">.</span><span class="nx">ok</span><span class="p">)</span> <span class="k">throw</span> <span class="nb">Error</span><span class="p">(</span><span class="nx">response</span><span class="p">.</span><span class="nx">statusText</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">savedEvent</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">response</span><span class="p">.</span><span class="nx">json</span><span class="p">();</span>
<span class="kd">const</span> <span class="nx">newEvents</span> <span class="o">=</span> <span class="p">[...</span><span class="nx">events</span><span class="p">,</span> <span class="nx">savedEvent</span><span class="p">];</span>
<span class="nx">setEvents</span><span class="p">(</span><span class="nx">newEvents</span><span class="p">);</span>
<span class="nb">window</span><span class="p">.</span><span class="nx">alert</span><span class="p">(</span><span class="dl">'</span><span class="s1">Event Added!</span><span class="dl">'</span><span class="p">);</span>
<span class="nx">navigate</span><span class="p">(</span><span class="s2">`/events/</span><span class="p">${</span><span class="nx">savedEvent</span><span class="p">.</span><span class="nx">id</span><span class="p">}</span><span class="s2">`</span><span class="p">);</span>
<span class="p">}</span> <span class="k">catch</span> <span class="p">(</span><span class="nx">error</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">error</span><span class="p">(</span><span class="nx">error</span><span class="p">);</span>
<span class="p">}</span>
<span class="p">};</span>
<span class="k">return</span> <span class="p">(</span>
<span class="p"><></span>
...
<span class="p"><</span><span class="nc">Routes</span><span class="p">></span>
<span class="p"><</span><span class="nc">Route</span> <span class="na">path</span><span class="p">=</span><span class="s">"new"</span> <span class="na">element</span><span class="p">=</span><span class="si">{</span><span class="p"><</span><span class="nc">EventForm</span> <span class="na">onSave</span><span class="p">=</span><span class="si">{</span><span class="nx">addEvent</span><span class="si">}</span> <span class="p">/></span><span class="si">}</span> <span class="p">/></span>
<span class="p"><</span><span class="nc">Route</span> <span class="na">path</span><span class="p">=</span><span class="s">":id"</span> <span class="na">element</span><span class="p">=</span><span class="si">{</span><span class="p"><</span><span class="nc">Event</span> <span class="na">events</span><span class="p">=</span><span class="si">{</span><span class="nx">events</span><span class="si">}</span> <span class="p">/></span><span class="si">}</span> <span class="p">/></span>
<span class="p"></</span><span class="nc">Routes</span><span class="p">></span>
...
<span class="p"></></span>
<span class="p">);</span>
<span class="p">};</span>
</code></pre></div></div>
<p>As you can see, we have defined an <code class="language-plaintext highlighter-rouge">addEvent</code> method, which receives a <code class="language-plaintext highlighter-rouge">newEvent</code> object and then fires off a request to our API to create a new event using that data. If the request is successful, it will add the newly created event to the array of events that are being held in state and the UI will update accordingly. It will also use the <code class="language-plaintext highlighter-rouge">navigate</code> function, which is made available to us by the <a href="https://reactrouterdotcom.fly.dev/docs/en/v6/api#usenavigate">useNavigate hook</a>, to change the URL to that of the newly created event.</p>
<p>In the case that the API request is not successful (e.g. network problems, server responds with an error code etc), the error is logged to the console.</p>
<p>Note also that we are passing the <code class="language-plaintext highlighter-rouge">addEvent</code> method into the <code class="language-plaintext highlighter-rouge">EventForm</code> component as a callback. Now, all we’ve got to do is call it in our <code class="language-plaintext highlighter-rouge"><EventForm></code> component:</p>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="nx">PropTypes</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">prop-types</span><span class="dl">'</span><span class="p">;</span>
<span class="p">...</span>
<span class="kd">const</span> <span class="nx">EventForm</span> <span class="o">=</span> <span class="p">({</span> <span class="nx">onSave</span> <span class="p">})</span> <span class="o">=></span> <span class="p">{</span>
<span class="p">...</span>
<span class="kd">const</span> <span class="nx">handleSubmit</span> <span class="o">=</span> <span class="p">(</span><span class="nx">e</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">e</span><span class="p">.</span><span class="nx">preventDefault</span><span class="p">();</span>
<span class="kd">const</span> <span class="nx">errors</span> <span class="o">=</span> <span class="nx">validateEvent</span><span class="p">(</span><span class="nx">event</span><span class="p">);</span>
<span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">isEmptyObject</span><span class="p">(</span><span class="nx">errors</span><span class="p">))</span> <span class="p">{</span>
<span class="nx">setFormErrors</span><span class="p">(</span><span class="nx">errors</span><span class="p">);</span>
<span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
<span class="nx">onSave</span><span class="p">(</span><span class="nx">event</span><span class="p">);</span>
<span class="p">}</span>
<span class="p">};</span>
<span class="k">return</span> <span class="p">(</span> <span class="p">...</span> <span class="p">);</span>
<span class="p">};</span>
<span class="k">export</span> <span class="k">default</span> <span class="nx">EventForm</span><span class="p">;</span>
<span class="nx">EventForm</span><span class="p">.</span><span class="nx">propTypes</span> <span class="o">=</span> <span class="p">{</span>
<span class="na">onSave</span><span class="p">:</span> <span class="nx">PropTypes</span><span class="p">.</span><span class="nx">func</span><span class="p">.</span><span class="nx">isRequired</span><span class="p">,</span>
<span class="p">};</span>
</code></pre></div></div>
<p>Now, when you attempt to save an event to the database, you should get an alert pop up, informing you that the save was successful. If you’re following along, I’d encourage you to give this a try and satisfy yourself that everything is working before continuing.</p>
<h2 id="deleting-events">Deleting Events</h2>
<p>Now, if you’re anything like me, you will have created a bunch of silly events while following along with this tutorial. Let’s add a delete button so that we can nuke them. 💥</p>
<p>As with adding an event, we’ll want to declare a method to delete an event in our <code class="language-plaintext highlighter-rouge"><Editor></code> component, then pass it to our <code class="language-plaintext highlighter-rouge"><Event></code> component as a prop.</p>
<p>First the method:</p>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">Editor</span> <span class="o">=</span> <span class="p">()</span> <span class="o">=></span> <span class="p">{</span>
<span class="p">...</span>
<span class="kd">const</span> <span class="nx">addEvent</span> <span class="o">=</span> <span class="k">async</span> <span class="p">(</span><span class="nx">newEvent</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span> <span class="p">...</span> <span class="p">};</span>
<span class="kd">const</span> <span class="nx">deleteEvent</span> <span class="o">=</span> <span class="k">async</span> <span class="p">(</span><span class="nx">eventId</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="kd">const</span> <span class="nx">sure</span> <span class="o">=</span> <span class="nb">window</span><span class="p">.</span><span class="nx">confirm</span><span class="p">(</span><span class="dl">'</span><span class="s1">Are you sure?</span><span class="dl">'</span><span class="p">);</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">sure</span><span class="p">)</span> <span class="p">{</span>
<span class="k">try</span> <span class="p">{</span>
<span class="kd">const</span> <span class="nx">response</span> <span class="o">=</span> <span class="k">await</span> <span class="nb">window</span><span class="p">.</span><span class="nx">fetch</span><span class="p">(</span><span class="s2">`/api/events/</span><span class="p">${</span><span class="nx">eventId</span><span class="p">}</span><span class="s2">`</span><span class="p">,</span> <span class="p">{</span>
<span class="na">method</span><span class="p">:</span> <span class="dl">'</span><span class="s1">DELETE</span><span class="dl">'</span><span class="p">,</span>
<span class="p">});</span>
<span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">response</span><span class="p">.</span><span class="nx">ok</span><span class="p">)</span> <span class="k">throw</span> <span class="nb">Error</span><span class="p">(</span><span class="nx">response</span><span class="p">.</span><span class="nx">statusText</span><span class="p">);</span>
<span class="nb">window</span><span class="p">.</span><span class="nx">alert</span><span class="p">(</span><span class="dl">'</span><span class="s1">Event Deleted!</span><span class="dl">'</span><span class="p">);</span>
<span class="nx">navigate</span><span class="p">(</span><span class="dl">'</span><span class="s1">/events</span><span class="dl">'</span><span class="p">);</span>
<span class="nx">setEvents</span><span class="p">(</span><span class="nx">events</span><span class="p">.</span><span class="nx">filter</span><span class="p">(</span><span class="nx">event</span> <span class="o">=></span> <span class="nx">event</span><span class="p">.</span><span class="nx">id</span> <span class="o">!==</span> <span class="nx">eventId</span><span class="p">));</span>
<span class="p">}</span> <span class="k">catch</span> <span class="p">(</span><span class="nx">error</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">error</span><span class="p">(</span><span class="nx">error</span><span class="p">);</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="p">};</span>
<span class="k">return</span> <span class="p">(</span> <span class="p">...</span> <span class="p">);</span>
<span class="p">};</span>
</code></pre></div></div>
<p>In our <code class="language-plaintext highlighter-rouge">deleteEvent</code> method, we ask the user for confirmation that they really want to delete the event via a confirm dialogue. If the user is sure, we send a DELETE request to our API and once a successful response comes back, we inform the user that the event has been deleted, redirect the user to <code class="language-plaintext highlighter-rouge">/events</code> and remove the deleted event from state. As with the <code class="language-plaintext highlighter-rouge">addEvent</code> method, if the request is unsuccessful, the error is logged to the console.</p>
<p>Next, pass the <code class="language-plaintext highlighter-rouge">deleteEvent</code> callback to the <code class="language-plaintext highlighter-rouge"><Event></code> component:</p>
<div class="language-jsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p"><</span><span class="nc">Routes</span><span class="p">></span>
<span class="p"><</span><span class="nc">Route</span> <span class="na">path</span><span class="p">=</span><span class="s">"new"</span> <span class="na">element</span><span class="p">=</span><span class="si">{</span><span class="p"><</span><span class="nc">EventForm</span> <span class="na">onSave</span><span class="p">=</span><span class="si">{</span><span class="nx">addEvent</span><span class="si">}</span> <span class="p">/></span><span class="si">}</span> <span class="p">/></span>
<span class="p"><</span><span class="nc">Route</span> <span class="na">path</span><span class="p">=</span><span class="s">":id"</span> <span class="na">element</span><span class="p">=</span><span class="si">{</span><span class="p"><</span><span class="nc">Event</span> <span class="na">events</span><span class="p">=</span><span class="si">{</span><span class="nx">events</span><span class="si">}</span> <span class="na">onDelete</span><span class="p">=</span><span class="si">{</span><span class="nx">deleteEvent</span><span class="si">}</span> <span class="p">/></span><span class="si">}</span> <span class="p">/></span>
<span class="p"></</span><span class="nc">Routes</span><span class="p">></span>
</code></pre></div></div>
<p>Now, in the <code class="language-plaintext highlighter-rouge"><Event></code> component we can create a button to delete the event. We’re not using a <code class="language-plaintext highlighter-rouge"><Link></code> component here (as we do for creating a new event), as this is not a hypertext link that can be followed or be crawled. To remain consistent with the styling though, I have styled it to look like a link in the CSS above.</p>
<div class="language-jsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">Event</span> <span class="o">=</span> <span class="p">({</span> <span class="nx">events</span><span class="p">,</span> <span class="nx">onDelete</span> <span class="p">})</span> <span class="o">=></span> <span class="p">{</span>
<span class="p">...</span>
<span class="k">return</span> <span class="p">(</span>
<span class="p"><</span><span class="nt">div</span> <span class="na">className</span><span class="p">=</span><span class="s">"eventContainer"</span><span class="p">></span>
<span class="p"><</span><span class="nt">h2</span><span class="p">></span>
<span class="si">{</span><span class="nx">event</span><span class="p">.</span><span class="nx">event_date</span><span class="si">}</span>
<span class="si">{</span><span class="dl">'</span><span class="s1"> - </span><span class="dl">'</span><span class="si">}</span>
<span class="si">{</span><span class="nx">event</span><span class="p">.</span><span class="nx">event_type</span><span class="si">}</span>
<span class="p"><</span><span class="nt">button</span>
<span class="na">className</span><span class="p">=</span><span class="s">"delete"</span>
<span class="na">type</span><span class="p">=</span><span class="s">"button"</span>
<span class="na">onClick</span><span class="p">=</span><span class="si">{</span><span class="p">()</span> <span class="o">=></span> <span class="nx">onDelete</span><span class="p">(</span><span class="nx">event</span><span class="p">.</span><span class="nx">id</span><span class="p">)</span><span class="si">}</span>
<span class="p">></span>
Delete
<span class="p"></</span><span class="nt">button</span><span class="p">></span>
<span class="p"></</span><span class="nt">h2</span><span class="p">></span>
...
<span class="p"></</span><span class="nt">div</span><span class="p">></span>
<span class="p">);</span>
<span class="p">};</span>
<span class="nx">Event</span><span class="p">.</span><span class="nx">propTypes</span> <span class="o">=</span> <span class="p">{</span>
<span class="na">events</span><span class="p">:</span> <span class="nx">PropTypes</span><span class="p">.</span><span class="nx">arrayOf</span><span class="p">(</span>
<span class="nx">PropTypes</span><span class="p">.</span><span class="nx">shape</span><span class="p">({</span> <span class="p">...</span> <span class="p">})</span>
<span class="p">).</span><span class="nx">isRequired</span><span class="p">,</span>
<span class="na">onDelete</span><span class="p">:</span> <span class="nx">PropTypes</span><span class="p">.</span><span class="nx">func</span><span class="p">.</span><span class="nx">isRequired</span><span class="p">,</span>
<span class="p">};</span>
</code></pre></div></div>
<p>And now we can delete events.</p>
<h2 id="adding-flash-messages">Adding Flash Messages</h2>
<p>Alerts are all well and good to tell the user that something happened, but they don’t look very pretty. Let’s add flash message functionality instead, using the <a href="https://www.npmjs.com/package/react-toastify">React-Toastify library</a>.</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>npm i react-toastify
</code></pre></div></div>
<p>Or with Yarn:</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>yarn add react-toastify
</code></pre></div></div>
<p>We’ll stick this functionality in its own helper file, <code class="language-plaintext highlighter-rouge">notifications.js</code>.</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">touch </span>app/javascript/helpers/notifications.js
</code></pre></div></div>
<p>Then add:</p>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">toast</span><span class="p">,</span> <span class="nx">Flip</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">react-toastify</span><span class="dl">'</span><span class="p">;</span>
<span class="k">import</span> <span class="dl">'</span><span class="s1">react-toastify/dist/ReactToastify.css</span><span class="dl">'</span><span class="p">;</span>
<span class="kd">const</span> <span class="nx">defaults</span> <span class="o">=</span> <span class="p">{</span>
<span class="na">position</span><span class="p">:</span> <span class="dl">'</span><span class="s1">top-right</span><span class="dl">'</span><span class="p">,</span>
<span class="na">autoClose</span><span class="p">:</span> <span class="mi">5000</span><span class="p">,</span>
<span class="na">hideProgressBar</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
<span class="na">closeOnClick</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
<span class="na">pauseOnHover</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
<span class="na">draggable</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
<span class="na">progress</span><span class="p">:</span> <span class="kc">undefined</span><span class="p">,</span>
<span class="na">transition</span><span class="p">:</span> <span class="nx">Flip</span><span class="p">,</span>
<span class="p">};</span>
<span class="k">export</span> <span class="kd">const</span> <span class="nx">success</span> <span class="o">=</span> <span class="p">(</span><span class="nx">message</span><span class="p">,</span> <span class="nx">options</span> <span class="o">=</span> <span class="p">{})</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">toast</span><span class="p">.</span><span class="nx">success</span><span class="p">(</span><span class="nx">message</span><span class="p">,</span> <span class="nb">Object</span><span class="p">.</span><span class="nx">assign</span><span class="p">(</span><span class="nx">defaults</span><span class="p">,</span> <span class="nx">options</span><span class="p">));</span>
<span class="p">};</span>
<span class="k">export</span> <span class="kd">const</span> <span class="nx">info</span> <span class="o">=</span> <span class="p">(</span><span class="nx">message</span><span class="p">,</span> <span class="nx">options</span> <span class="o">=</span> <span class="p">{})</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">toast</span><span class="p">.</span><span class="nx">info</span><span class="p">(</span><span class="nx">message</span><span class="p">,</span> <span class="nb">Object</span><span class="p">.</span><span class="nx">assign</span><span class="p">(</span><span class="nx">defaults</span><span class="p">,</span> <span class="nx">options</span><span class="p">));</span>
<span class="p">};</span>
<span class="k">export</span> <span class="kd">const</span> <span class="nx">warn</span> <span class="o">=</span> <span class="p">(</span><span class="nx">message</span><span class="p">,</span> <span class="nx">options</span> <span class="o">=</span> <span class="p">{})</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">toast</span><span class="p">.</span><span class="nx">warn</span><span class="p">(</span><span class="nx">message</span><span class="p">,</span> <span class="nb">Object</span><span class="p">.</span><span class="nx">assign</span><span class="p">(</span><span class="nx">defaults</span><span class="p">,</span> <span class="nx">options</span><span class="p">));</span>
<span class="p">};</span>
<span class="k">export</span> <span class="kd">const</span> <span class="nx">error</span> <span class="o">=</span> <span class="p">(</span><span class="nx">message</span><span class="p">,</span> <span class="nx">options</span> <span class="o">=</span> <span class="p">{})</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">toast</span><span class="p">.</span><span class="nx">error</span><span class="p">(</span><span class="nx">message</span><span class="p">,</span> <span class="nb">Object</span><span class="p">.</span><span class="nx">assign</span><span class="p">(</span><span class="nx">defaults</span><span class="p">,</span> <span class="nx">options</span><span class="p">));</span>
<span class="p">};</span>
</code></pre></div></div>
<p>Now we have a centralized place to set some sensible defaults and can reduce the boilerplate when calling the flash messages. React-Toastify is highly configurable, so if you’d like to see what else the library can do, head on over to their <a href="https://fkhadra.github.io/react-toastify/introduction/">playground</a> and experiment with the notifications until you find a style you like. Also be sure to check out the different <a href="https://fkhadra.github.io/react-toastify/replace-default-transition/">transition effects</a>, as these are configurable, too.</p>
<p>Next, include the library in the <code class="language-plaintext highlighter-rouge"><App></code> component:</p>
<div class="language-jsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">ToastContainer</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">react-toastify</span><span class="dl">'</span><span class="p">;</span>
<span class="p">...</span>
<span class="kd">const</span> <span class="nx">App</span> <span class="o">=</span> <span class="p">()</span> <span class="o">=></span> <span class="p">(</span>
<span class="p"><></span>
<span class="p"><</span><span class="nc">Routes</span><span class="p">></span>
<span class="p"><</span><span class="nc">Route</span> <span class="na">path</span><span class="p">=</span><span class="s">"events/*"</span> <span class="na">element</span><span class="p">=</span><span class="si">{</span><span class="p"><</span><span class="nc">Editor</span> <span class="p">/></span><span class="si">}</span> <span class="p">/></span>
<span class="p"></</span><span class="nc">Routes</span><span class="p">></span>
<span class="p"><</span><span class="nc">ToastContainer</span> <span class="p">/></span>
<span class="p"></></span>
<span class="p">);</span>
</code></pre></div></div>
<p>Here we are adding a <a href="https://fkhadra.github.io/react-toastify/api/toast-container">ToastContainer</a> to our app, for the flash messages (or toast messages, I guess I should say) to display.</p>
<p>Then alter the <code class="language-plaintext highlighter-rouge"><Editor></code> component to replace our alerts:</p>
<div class="language-diff highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">import { success } from '../helpers/notifications';
</span><span class="err">...</span>
const addEvent = async (newEvent) => {
try {
...
<span class="gd">- window.alert('Event Added!);
</span><span class="gi">+ success('Event Added!');
</span> ...
} catch (error) {
console.error(error);
}
<span class="err">};</span>
<span class="p">const deleteEvent = async (eventId) => {
</span> const sure = window.confirm('Are you sure?');
if (sure) {
try {
...
<span class="gd">- window.alert('Event Deleted!);
</span><span class="gi">+ success('Event Deleted!');
</span> ...
} catch (error) {
console.error(error);
}
}
<span class="err">};</span>
</code></pre></div></div>
<p>While we’re at it we can move the error handling into a helper method, too. In <code class="language-plaintext highlighter-rouge">app/javascript/helpers/helpers.js</code>:</p>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">error</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">./notifications</span><span class="dl">'</span><span class="p">;</span>
<span class="p">...</span>
<span class="k">export</span> <span class="kd">const</span> <span class="nx">handleAjaxError</span> <span class="o">=</span> <span class="p">(</span><span class="nx">err</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">error</span><span class="p">(</span><span class="dl">'</span><span class="s1">Something went wrong</span><span class="dl">'</span><span class="p">);</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">error</span><span class="p">(</span><span class="nx">err</span><span class="p">);</span>
<span class="p">};</span>
</code></pre></div></div>
<p>And in the <code class="language-plaintext highlighter-rouge"><Editor></code> component:</p>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">handleAjaxError</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">../helpers/helpers</span><span class="dl">'</span><span class="p">;</span>
</code></pre></div></div>
<p>And replace the three occurrences of:</p>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">console</span><span class="p">.</span><span class="nx">error</span><span class="p">(</span><span class="nx">error</span><span class="p">);</span>
</code></pre></div></div>
<p>With:</p>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">handleAjaxError</span><span class="p">(</span><span class="nx">error</span><span class="p">);</span>
</code></pre></div></div>
<p>The following lines can also be removed from the component, as they are no longer needed.</p>
<div class="language-diff highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gd">- const [isError, setIsError] = useState(false);
- setIsError(true);
- {isError && <p>Something went wrong. Check the console.</p>}
</span></code></pre></div></div>
<p>Now, when you create or delete an event, you should get a nicely styled flash message.</p>
<p><img src="https://res.cloudinary.com/hibbard/image/upload/v1648224360/event-manager-hooks/11-flash-message-event-added.png" alt="Event Manager - Flash message" /></p>
<blockquote>
<p>Note that if you are testing the error message functionality (e.g. by making a typo in an endpoint), you will see two flash messages telling you that something has gone wrong, where you might have expected one. The reason for this is the use of <code class="language-plaintext highlighter-rouge">StrictMode</code>, which renders components twice (in development only) so as to detect any problems with your code and warn you accordingly.</p>
</blockquote>
<h2 id="updating-an-event">Updating an Event</h2>
<p>The final piece of our CRUD functionality to add is the ability to update an event. To avoid duplication we’re going to reuse our <code class="language-plaintext highlighter-rouge"><EventForm></code> component. If we pass it a list of events, it should grab the event ID from the URL, find the correct event from the list, then prepopulate the form fields with the correct values. We will also define an <code class="language-plaintext highlighter-rouge">updateEvent</code> function that will allow us to perform a different action when the user hits the <em>Save</em> button, depending on whether we are creating an event, or updating an existing one.</p>
<p>Let’s start by adding the <em>Edit</em> link to the <code class="language-plaintext highlighter-rouge"><Event></code> component. It’s fine to make this a link, as it will change the URL:</p>
<div class="language-jsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">useParams</span><span class="p">,</span> <span class="nx">Link</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">react-router-dom</span><span class="dl">'</span><span class="p">;</span>
<span class="p">...</span>
<span class="o"><</span><span class="nx">h2</span><span class="o">></span>
<span class="p">{</span><span class="nx">event</span><span class="p">.</span><span class="nx">event_date</span><span class="p">}</span>
<span class="p">{</span><span class="dl">'</span><span class="s1"> - </span><span class="dl">'</span><span class="p">}</span>
<span class="p">{</span><span class="nx">event</span><span class="p">.</span><span class="nx">event_type</span><span class="p">}</span>
<span class="p"><</span><span class="nc">Link</span> <span class="na">to</span><span class="p">=</span><span class="si">{</span><span class="s2">`/events/</span><span class="p">${</span><span class="nx">event</span><span class="p">.</span><span class="nx">id</span><span class="p">}</span><span class="s2">/edit`</span><span class="si">}</span><span class="p">></span>Edit<span class="p"></</span><span class="nc">Link</span><span class="p">></span>
<span class="p"><</span><span class="nt">button</span>
<span class="na">className</span><span class="p">=</span><span class="s">"delete"</span>
<span class="na">type</span><span class="p">=</span><span class="s">"button"</span>
<span class="na">onClick</span><span class="p">=</span><span class="si">{</span><span class="p">()</span> <span class="o">=></span> <span class="nx">onDelete</span><span class="p">(</span><span class="nx">event</span><span class="p">.</span><span class="nx">id</span><span class="p">)</span><span class="si">}</span>
<span class="p">></span>
Delete
<span class="p"></</span><span class="nt">button</span><span class="p">></span>
<span class="p"><</span><span class="err">/</span><span class="na">h2</span><span class="p">></span>
</code></pre></div></div>
<p>In the <code class="language-plaintext highlighter-rouge"><Editor></code> component, let’s add the <code class="language-plaintext highlighter-rouge">updateEvent</code> function :</p>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">updateEvent</span> <span class="o">=</span> <span class="k">async</span> <span class="p">(</span><span class="nx">updatedEvent</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="k">try</span> <span class="p">{</span>
<span class="kd">const</span> <span class="nx">response</span> <span class="o">=</span> <span class="k">await</span> <span class="nb">window</span><span class="p">.</span><span class="nx">fetch</span><span class="p">(</span>
<span class="s2">`/api/events/</span><span class="p">${</span><span class="nx">updatedEvent</span><span class="p">.</span><span class="nx">id</span><span class="p">}</span><span class="s2">`</span><span class="p">,</span>
<span class="p">{</span>
<span class="na">method</span><span class="p">:</span> <span class="dl">'</span><span class="s1">PUT</span><span class="dl">'</span><span class="p">,</span>
<span class="na">body</span><span class="p">:</span> <span class="nx">JSON</span><span class="p">.</span><span class="nx">stringify</span><span class="p">(</span><span class="nx">updatedEvent</span><span class="p">),</span>
<span class="na">headers</span><span class="p">:</span> <span class="p">{</span>
<span class="na">Accept</span><span class="p">:</span> <span class="dl">'</span><span class="s1">application/json</span><span class="dl">'</span><span class="p">,</span>
<span class="dl">'</span><span class="s1">Content-Type</span><span class="dl">'</span><span class="p">:</span> <span class="dl">'</span><span class="s1">application/json</span><span class="dl">'</span><span class="p">,</span>
<span class="p">},</span>
<span class="p">}</span>
<span class="p">);</span>
<span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">response</span><span class="p">.</span><span class="nx">ok</span><span class="p">)</span> <span class="k">throw</span> <span class="nb">Error</span><span class="p">(</span><span class="nx">response</span><span class="p">.</span><span class="nx">statusText</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">newEvents</span> <span class="o">=</span> <span class="nx">events</span><span class="p">;</span>
<span class="kd">const</span> <span class="nx">idx</span> <span class="o">=</span> <span class="nx">newEvents</span><span class="p">.</span><span class="nx">findIndex</span><span class="p">((</span><span class="nx">event</span><span class="p">)</span> <span class="o">=></span> <span class="nx">event</span><span class="p">.</span><span class="nx">id</span> <span class="o">===</span> <span class="nx">updatedEvent</span><span class="p">.</span><span class="nx">id</span><span class="p">);</span>
<span class="nx">newEvents</span><span class="p">[</span><span class="nx">idx</span><span class="p">]</span> <span class="o">=</span> <span class="nx">updatedEvent</span><span class="p">;</span>
<span class="nx">setEvents</span><span class="p">(</span><span class="nx">newEvents</span><span class="p">);</span>
<span class="nx">success</span><span class="p">(</span><span class="dl">'</span><span class="s1">Event Updated!</span><span class="dl">'</span><span class="p">);</span>
<span class="nx">navigate</span><span class="p">(</span><span class="s2">`/events/</span><span class="p">${</span><span class="nx">updatedEvent</span><span class="p">.</span><span class="nx">id</span><span class="p">}</span><span class="s2">`</span><span class="p">);</span>
<span class="p">}</span> <span class="k">catch</span> <span class="p">(</span><span class="nx">error</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">handleAjaxError</span><span class="p">(</span><span class="nx">error</span><span class="p">);</span>
<span class="p">}</span>
<span class="p">};</span>
</code></pre></div></div>
<p>Hopefully, this is starting to feel a little familiar by now. Here we send a <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/PUT">PUT request</a> to the API with the updated event data. If we get a good response back (i.e. no error) then we create a <code class="language-plaintext highlighter-rouge">newEvents</code> variable, setting it to the current value of <code class="language-plaintext highlighter-rouge">events</code>. We then determine the index of the updated event inside the <code class="language-plaintext highlighter-rouge">newEvents</code> array and swap out the old event with the new one. Finally, we update the value of <code class="language-plaintext highlighter-rouge">events</code> in state, before showing the user a success message and programatically navigating to the new event.</p>
<p>We go through the trouble of creating an additional variable, as it is good practice not to mutate state directly.</p>
<p>Next, let’s declare a new route for editing events which will render our <code class="language-plaintext highlighter-rouge"><EventForm></code> component:</p>
<div class="language-jsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p"><</span><span class="nc">Routes</span><span class="p">></span>
<span class="p"><</span><span class="nc">Route</span>
<span class="na">path</span><span class="p">=</span><span class="s">":id"</span>
<span class="na">element</span><span class="p">=</span><span class="si">{</span><span class="p"><</span><span class="nc">Event</span> <span class="na">events</span><span class="p">=</span><span class="si">{</span><span class="nx">events</span><span class="si">}</span> <span class="na">onDelete</span><span class="p">=</span><span class="si">{</span><span class="nx">deleteEvent</span><span class="si">}</span> <span class="p">/></span><span class="si">}</span>
<span class="p">/></span>
<span class="p"><</span><span class="nc">Route</span>
<span class="na">path</span><span class="p">=</span><span class="s">":id/edit"</span>
<span class="na">element</span><span class="p">=</span><span class="si">{</span><span class="p"><</span><span class="nc">EventForm</span> <span class="na">events</span><span class="p">=</span><span class="si">{</span><span class="nx">events</span><span class="si">}</span> <span class="na">onSave</span><span class="p">=</span><span class="si">{</span><span class="nx">updateEvent</span><span class="si">}</span> <span class="p">/></span><span class="si">}</span>
<span class="p">/></span>
<span class="p"><</span><span class="nc">Route</span> <span class="na">path</span><span class="p">=</span><span class="s">"new"</span> <span class="na">element</span><span class="p">=</span><span class="si">{</span><span class="p"><</span><span class="nc">EventForm</span> <span class="na">onSave</span><span class="p">=</span><span class="si">{</span><span class="nx">addEvent</span><span class="si">}</span> <span class="p">/></span><span class="si">}</span> <span class="p">/></span>
<span class="p"></</span><span class="nc">Routes</span><span class="p">></span>
</code></pre></div></div>
<p>In the <code class="language-plaintext highlighter-rouge"><EventForm></code> component, we need to make sure that the form fields are populated with the correct values, whenever we pass it a list of events.</p>
<p>Remove the following code from the component:</p>
<div class="language-diff highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gd">- const [event, setEvent] = useState({
- event_type: '',
- event_date: '',
- title: '',
- speaker: '',
- host: '',
- published: false,
- });
</span></code></pre></div></div>
<p>Then add the following:</p>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">useParams</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">react-router-dom</span><span class="dl">'</span><span class="p">;</span>
<span class="p">...</span>
<span class="kd">const</span> <span class="nx">EventForm</span> <span class="o">=</span> <span class="p">({</span> <span class="nx">events</span><span class="p">,</span> <span class="nx">onSave</span> <span class="p">})</span> <span class="o">=></span> <span class="p">{</span>
<span class="kd">const</span> <span class="p">{</span> <span class="nx">id</span> <span class="p">}</span> <span class="o">=</span> <span class="nx">useParams</span><span class="p">();</span>
<span class="kd">const</span> <span class="nx">defaults</span> <span class="o">=</span> <span class="p">{</span>
<span class="na">event_type</span><span class="p">:</span> <span class="dl">''</span><span class="p">,</span>
<span class="na">event_date</span><span class="p">:</span> <span class="dl">''</span><span class="p">,</span>
<span class="na">title</span><span class="p">:</span> <span class="dl">''</span><span class="p">,</span>
<span class="na">speaker</span><span class="p">:</span> <span class="dl">''</span><span class="p">,</span>
<span class="na">host</span><span class="p">:</span> <span class="dl">''</span><span class="p">,</span>
<span class="na">published</span><span class="p">:</span> <span class="kc">false</span><span class="p">,</span>
<span class="p">}</span>
<span class="kd">const</span> <span class="nx">currEvent</span> <span class="o">=</span> <span class="nx">id</span><span class="p">?</span> <span class="nx">events</span><span class="p">.</span><span class="nx">find</span><span class="p">((</span><span class="nx">e</span><span class="p">)</span> <span class="o">=></span> <span class="nx">e</span><span class="p">.</span><span class="nx">id</span> <span class="o">===</span> <span class="nb">Number</span><span class="p">(</span><span class="nx">id</span><span class="p">))</span> <span class="p">:</span> <span class="p">{};</span>
<span class="kd">const</span> <span class="nx">initialEventState</span> <span class="o">=</span> <span class="p">{</span> <span class="p">...</span><span class="nx">defaults</span><span class="p">,</span> <span class="p">...</span><span class="nx">currEvent</span> <span class="p">}</span>
<span class="kd">const</span> <span class="p">[</span><span class="nx">event</span><span class="p">,</span> <span class="nx">setEvent</span><span class="p">]</span> <span class="o">=</span> <span class="nx">useState</span><span class="p">(</span><span class="nx">initialEventState</span><span class="p">);</span>
<span class="p">...</span>
<span class="p">}</span>
</code></pre></div></div>
<p>Here we are using React Router’s useParams hook to retrieve the ID of the current event from the URL. This will either be an integer or, when the form is being used to create a new event, <code class="language-plaintext highlighter-rouge">undefined</code>. We then declare some sensible defaults for our event fields.</p>
<p>Next we check the value of the <code class="language-plaintext highlighter-rouge">id</code> variable. If it is <code class="language-plaintext highlighter-rouge">undefined</code> (we are creating a new event), we set <code class="language-plaintext highlighter-rouge">currEvent</code> to be an empty object. Otherwise, we filter our array of events to find the event we are updating, and set the value of <code class="language-plaintext highlighter-rouge">currEvent</code> to that.</p>
<p>We then merge <code class="language-plaintext highlighter-rouge">defaults</code> and <code class="language-plaintext highlighter-rouge">currEvent</code> into a new variable called <code class="language-plaintext highlighter-rouge">initialEventState</code> before declaring an <code class="language-plaintext highlighter-rouge">event</code> property in state and initializing it with the value of <code class="language-plaintext highlighter-rouge">initialEventState</code>.</p>
<p>This has the <code class="language-plaintext highlighter-rouge">effect</code> of event being initialized with some sensible defaults, or the value of the event we wish to edit.</p>
<p>This might seem like a bit of a convoluted way to do things and ideally I would have liked to pass the <code class="language-plaintext highlighter-rouge"><EventForm></code> component only the event it needed to display. However, since upgrading to React router 6, I have been unable to find a way to reference the <code class="language-plaintext highlighter-rouge">:id</code> property in the parent component while keeping the routes as they are. If anyone has a suggestion as to how to do this, I’d be more than happy to hear it in the comments bellow.</p>
<p>Next, we need to make sure the form is initialized with the correct values from <code class="language-plaintext highlighter-rouge">event</code> and we need to update the component’s prop validation:</p>
<div class="language-jsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">return</span> <span class="p">(</span>
<span class="p"><</span><span class="nt">div</span><span class="p">></span>
<span class="p"><</span><span class="nt">h2</span><span class="p">></span>New Event<span class="p"></</span><span class="nt">h2</span><span class="p">></span>
<span class="si">{</span><span class="nx">renderErrors</span><span class="p">()</span><span class="si">}</span>
<span class="p"><</span><span class="nt">form</span> <span class="na">className</span><span class="p">=</span><span class="s">"eventForm"</span> <span class="na">onSubmit</span><span class="p">=</span><span class="si">{</span><span class="nx">handleSubmit</span><span class="si">}</span><span class="p">></span>
<span class="p"><</span><span class="nt">div</span><span class="p">></span>
<span class="p"><</span><span class="nt">label</span> <span class="na">htmlFor</span><span class="p">=</span><span class="s">"event_type"</span><span class="p">></span>
<span class="p"><</span><span class="nt">strong</span><span class="p">></span>Type:<span class="p"></</span><span class="nt">strong</span><span class="p">></span>
<span class="p"><</span><span class="nt">input</span>
<span class="na">type</span><span class="p">=</span><span class="s">"text"</span>
<span class="na">id</span><span class="p">=</span><span class="s">"event_type"</span>
<span class="na">name</span><span class="p">=</span><span class="s">"event_type"</span>
<span class="na">onChange</span><span class="p">=</span><span class="si">{</span><span class="nx">handleInputChange</span><span class="si">}</span>
<span class="na">value</span><span class="p">=</span><span class="si">{</span><span class="nx">event</span><span class="p">.</span><span class="nx">event_type</span><span class="si">}</span>
<span class="p">/></span>
<span class="p"></</span><span class="nt">label</span><span class="p">></span>
<span class="p"></</span><span class="nt">div</span><span class="p">></span>
<span class="p"><</span><span class="nt">div</span><span class="p">></span>
<span class="p"><</span><span class="nt">label</span> <span class="na">htmlFor</span><span class="p">=</span><span class="s">"event_date"</span><span class="p">></span>
<span class="p"><</span><span class="nt">strong</span><span class="p">></span>Date:<span class="p"></</span><span class="nt">strong</span><span class="p">></span>
<span class="p"><</span><span class="nt">input</span>
<span class="na">type</span><span class="p">=</span><span class="s">"text"</span>
<span class="na">id</span><span class="p">=</span><span class="s">"event_date"</span>
<span class="na">name</span><span class="p">=</span><span class="s">"event_date"</span>
<span class="na">ref</span><span class="p">=</span><span class="si">{</span><span class="nx">dateInput</span><span class="si">}</span>
<span class="na">autoComplete</span><span class="p">=</span><span class="s">"off"</span>
<span class="na">value</span><span class="p">=</span><span class="si">{</span><span class="nx">event</span><span class="p">.</span><span class="nx">event_date</span><span class="si">}</span>
<span class="na">onChange</span><span class="p">=</span><span class="si">{</span><span class="nx">handleInputChange</span><span class="si">}</span>
<span class="p">/></span>
<span class="p"></</span><span class="nt">label</span><span class="p">></span>
<span class="p"></</span><span class="nt">div</span><span class="p">></span>
<span class="p"><</span><span class="nt">div</span><span class="p">></span>
<span class="p"><</span><span class="nt">label</span> <span class="na">htmlFor</span><span class="p">=</span><span class="s">"title"</span><span class="p">></span>
<span class="p"><</span><span class="nt">strong</span><span class="p">></span>Title:<span class="p"></</span><span class="nt">strong</span><span class="p">></span>
<span class="p"><</span><span class="nt">textarea</span>
<span class="na">cols</span><span class="p">=</span><span class="s">"30"</span>
<span class="na">rows</span><span class="p">=</span><span class="s">"10"</span>
<span class="na">id</span><span class="p">=</span><span class="s">"title"</span>
<span class="na">name</span><span class="p">=</span><span class="s">"title"</span>
<span class="na">onChange</span><span class="p">=</span><span class="si">{</span><span class="nx">handleInputChange</span><span class="si">}</span>
<span class="na">value</span><span class="p">=</span><span class="si">{</span><span class="nx">event</span><span class="p">.</span><span class="nx">title</span><span class="si">}</span>
<span class="p">/></span>
<span class="p"></</span><span class="nt">label</span><span class="p">></span>
<span class="p"></</span><span class="nt">div</span><span class="p">></span>
<span class="p"><</span><span class="nt">div</span><span class="p">></span>
<span class="p"><</span><span class="nt">label</span> <span class="na">htmlFor</span><span class="p">=</span><span class="s">"speaker"</span><span class="p">></span>
<span class="p"><</span><span class="nt">strong</span><span class="p">></span>Speakers:<span class="p"></</span><span class="nt">strong</span><span class="p">></span>
<span class="p"><</span><span class="nt">input</span>
<span class="na">type</span><span class="p">=</span><span class="s">"text"</span>
<span class="na">id</span><span class="p">=</span><span class="s">"speaker"</span>
<span class="na">name</span><span class="p">=</span><span class="s">"speaker"</span>
<span class="na">onChange</span><span class="p">=</span><span class="si">{</span><span class="nx">handleInputChange</span><span class="si">}</span>
<span class="na">value</span><span class="p">=</span><span class="si">{</span><span class="nx">event</span><span class="p">.</span><span class="nx">speaker</span><span class="si">}</span>
<span class="p">/></span>
<span class="p"></</span><span class="nt">label</span><span class="p">></span>
<span class="p"></</span><span class="nt">div</span><span class="p">></span>
<span class="p"><</span><span class="nt">div</span><span class="p">></span>
<span class="p"><</span><span class="nt">label</span> <span class="na">htmlFor</span><span class="p">=</span><span class="s">"host"</span><span class="p">></span>
<span class="p"><</span><span class="nt">strong</span><span class="p">></span>Hosts:<span class="p"></</span><span class="nt">strong</span><span class="p">></span>
<span class="p"><</span><span class="nt">input</span>
<span class="na">type</span><span class="p">=</span><span class="s">"text"</span>
<span class="na">id</span><span class="p">=</span><span class="s">"host"</span>
<span class="na">name</span><span class="p">=</span><span class="s">"host"</span>
<span class="na">onChange</span><span class="p">=</span><span class="si">{</span><span class="nx">handleInputChange</span><span class="si">}</span>
<span class="na">value</span><span class="p">=</span><span class="si">{</span><span class="nx">event</span><span class="p">.</span><span class="nx">host</span><span class="si">}</span>
<span class="p">/></span>
<span class="p"></</span><span class="nt">label</span><span class="p">></span>
<span class="p"></</span><span class="nt">div</span><span class="p">></span>
<span class="p"><</span><span class="nt">div</span><span class="p">></span>
<span class="p"><</span><span class="nt">label</span> <span class="na">htmlFor</span><span class="p">=</span><span class="s">"published"</span><span class="p">></span>
<span class="p"><</span><span class="nt">strong</span><span class="p">></span>Publish:<span class="p"></</span><span class="nt">strong</span><span class="p">></span>
<span class="p"><</span><span class="nt">input</span>
<span class="na">type</span><span class="p">=</span><span class="s">"checkbox"</span>
<span class="na">id</span><span class="p">=</span><span class="s">"published"</span>
<span class="na">name</span><span class="p">=</span><span class="s">"published"</span>
<span class="na">onChange</span><span class="p">=</span><span class="si">{</span><span class="nx">handleInputChange</span><span class="si">}</span>
<span class="na">checked</span><span class="p">=</span><span class="si">{</span><span class="nx">event</span><span class="p">.</span><span class="nx">published</span><span class="si">}</span>
<span class="p">/></span>
<span class="p"></</span><span class="nt">label</span><span class="p">></span>
<span class="p"></</span><span class="nt">div</span><span class="p">></span>
<span class="p"><</span><span class="nt">div</span> <span class="na">className</span><span class="p">=</span><span class="s">"form-actions"</span><span class="p">></span>
<span class="p"><</span><span class="nt">button</span> <span class="na">type</span><span class="p">=</span><span class="s">"submit"</span><span class="p">></span>Save<span class="p"></</span><span class="nt">button</span><span class="p">></span>
<span class="p"></</span><span class="nt">div</span><span class="p">></span>
<span class="p"></</span><span class="nt">form</span><span class="p">></span>
<span class="p"></</span><span class="nt">div</span><span class="p">></span>
<span class="p">);</span>
<span class="p">...</span>
<span class="nx">EventForm</span><span class="p">.</span><span class="nx">propTypes</span> <span class="o">=</span> <span class="p">{</span>
<span class="na">events</span><span class="p">:</span> <span class="nx">PropTypes</span><span class="p">.</span><span class="nx">arrayOf</span><span class="p">(</span>
<span class="nx">PropTypes</span><span class="p">.</span><span class="nx">shape</span><span class="p">({</span>
<span class="na">id</span><span class="p">:</span> <span class="nx">PropTypes</span><span class="p">.</span><span class="nx">number</span><span class="p">.</span><span class="nx">isRequired</span><span class="p">,</span>
<span class="na">event_type</span><span class="p">:</span> <span class="nx">PropTypes</span><span class="p">.</span><span class="nx">string</span><span class="p">.</span><span class="nx">isRequired</span><span class="p">,</span>
<span class="na">event_date</span><span class="p">:</span> <span class="nx">PropTypes</span><span class="p">.</span><span class="nx">string</span><span class="p">.</span><span class="nx">isRequired</span><span class="p">,</span>
<span class="na">title</span><span class="p">:</span> <span class="nx">PropTypes</span><span class="p">.</span><span class="nx">string</span><span class="p">.</span><span class="nx">isRequired</span><span class="p">,</span>
<span class="na">speaker</span><span class="p">:</span> <span class="nx">PropTypes</span><span class="p">.</span><span class="nx">string</span><span class="p">.</span><span class="nx">isRequired</span><span class="p">,</span>
<span class="na">host</span><span class="p">:</span> <span class="nx">PropTypes</span><span class="p">.</span><span class="nx">string</span><span class="p">.</span><span class="nx">isRequired</span><span class="p">,</span>
<span class="na">published</span><span class="p">:</span> <span class="nx">PropTypes</span><span class="p">.</span><span class="nx">bool</span><span class="p">.</span><span class="nx">isRequired</span><span class="p">,</span>
<span class="p">})</span>
<span class="p">),</span>
<span class="na">onSave</span><span class="p">:</span> <span class="nx">PropTypes</span><span class="p">.</span><span class="nx">func</span><span class="p">.</span><span class="nx">isRequired</span><span class="p">,</span>
<span class="p">};</span>
<span class="nx">EventForm</span><span class="p">.</span><span class="nx">defaultProps</span> <span class="o">=</span> <span class="p">{</span>
<span class="na">events</span><span class="p">:</span> <span class="p">[],</span>
<span class="p">};</span>
</code></pre></div></div>
<p>We also need to pass the datepicker a <code class="language-plaintext highlighter-rouge">toString</code> function in its initial configuration, so that the date is formatted properly when the event date is added to the form:</p>
<div class="language-diff highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">useEffect(() => {
</span> const p = new Pikaday({
field: dateInput.current,
<span class="gi">+ toString: date => formatDate(date),
</span> onSelect: (date) => { ... },
});
// Return a cleanup function.
// React will call this prior to unmounting.
return () => p.destroy();
<span class="err">},</span> []);
</code></pre></div></div>
<p>And finally, we need to add another <code class="language-plaintext highlighter-rouge">useEffect</code> hook to ensure that the fields are cleared when a user is editing an event, then clicks <em>New Event</em>. This would have been accomplished by using the <code class="language-plaintext highlighter-rouge">componentWillReceiveProps</code> lifecycle method in a class-based component.</p>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">useEffect</span><span class="p">(()</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">setEvent</span><span class="p">(</span><span class="nx">initialEventState</span><span class="p">);</span>
<span class="p">},</span> <span class="p">[</span><span class="nx">events</span><span class="p">]);</span>
</code></pre></div></div>
<p>Notice that we are specifying the prop that we want to watch for changes in the dependency array.</p>
<p>And that’s that. We now have all of our CRUD functionality. Open the app and satisfy yourself that everything is working before moving on.</p>
<h3 id="dealing-with-the-eslint-warning">Dealing With the ESLint Warning</h3>
<p>If you installed ESLint earlier, you’ll notice a warning if at this point we lint our code.</p>
<p><img src="https://res.cloudinary.com/hibbard/image/upload/v1648390436/event-manager-hooks/12-eslint-warning-in-console.png" alt="React Hook useEffect has a missing dependency: 'initialEventState'. Either include it or remove the dependency array" />
So what is going on here?</p>
<p>Well, what the warning is telling us is that our second <code class="language-plaintext highlighter-rouge">useEffect</code> hook is using a value which could potentially change between renders. It’s complaining about <code class="language-plaintext highlighter-rouge">initialEventState</code> which is defined like so:</p>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">defaults</span> <span class="o">=</span> <span class="p">{</span> <span class="p">...</span> <span class="p">};</span>
<span class="kd">const</span> <span class="nx">currEvent</span> <span class="o">=</span> <span class="nx">id</span><span class="p">?</span> <span class="nx">events</span><span class="p">.</span><span class="nx">find</span><span class="p">((</span><span class="nx">e</span><span class="p">)</span> <span class="o">=></span> <span class="nx">e</span><span class="p">.</span><span class="nx">id</span> <span class="o">===</span> <span class="nb">Number</span><span class="p">(</span><span class="nx">id</span><span class="p">))</span> <span class="p">:</span> <span class="p">{};</span>
<span class="kd">const</span> <span class="nx">initialEventState</span> <span class="o">=</span> <span class="p">{</span> <span class="p">...</span><span class="nx">defaults</span><span class="p">,</span> <span class="p">...</span><span class="nx">currEvent</span> <span class="p">}</span>
</code></pre></div></div>
<p>The culprit is <code class="language-plaintext highlighter-rouge">currEvent</code> whose value is dependent on both <code class="language-plaintext highlighter-rouge">id</code> and <code class="language-plaintext highlighter-rouge">events</code>.</p>
<p>Now we could just switch this warning off, like so:</p>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// eslint-disable-next-line react-hooks/exhaustive-deps</span>
<span class="nx">useEffect</span><span class="p">(()</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">setEvent</span><span class="p">(</span><span class="nx">initialEventState</span><span class="p">);</span>
<span class="p">},</span> <span class="p">[</span><span class="nx">events</span><span class="p">]);</span>
</code></pre></div></div>
<p>But the React team explicitly <a href="https://github.com/facebook/create-react-app/issues/6880#issuecomment-485912528">advise against doing this</a>.</p>
<p>If we follow the advice in the warning above and move <code class="language-plaintext highlighter-rouge">initialState</code> inside the dependency array like so:</p>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">useEffect</span><span class="p">(()</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">setEvent</span><span class="p">(</span><span class="nx">initialEventState</span><span class="p">);</span>
<span class="p">},</span> <span class="p">[</span><span class="nx">events</span><span class="p">,</span> <span class="nx">initialEventState</span><span class="p">]);</span>
</code></pre></div></div>
<p>It just gives us a different warning:</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>The <span class="s1">'initialEventState'</span> object makes the dependencies of useEffect Hook <span class="o">(</span>at line 58<span class="o">)</span> change on every render.
To fix this, wrap the initialization of <span class="s1">'initialEventState'</span> <span class="k">in </span>its own useMemo<span class="o">()</span> Hook.
</code></pre></div></div>
<p>And, as a nasty side-effect, we can no-longer use our form, as the form reverts to its initial state after every key stroke.</p>
<p>To fix this up, we can employ a <a href="https://reactjs.org/docs/hooks-reference.html#usecallback">useCallback hook</a>, like so:</p>
<p>Delete the following code from the component:</p>
<div class="language-diff highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gd">- const defaults = {
- event_type: '',
- event_date: '',
- title: '',
- speaker: '',
- host: '',
- published: false,
- }
- const currEvent = id? events.find((e) => e.id === Number(id)) : {};
- const initialEventState = { ...defaults, ...currEvent }
</span></code></pre></div></div>
<p>And replace it with the following:</p>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">initialEventState</span> <span class="o">=</span> <span class="nx">useCallback</span><span class="p">(</span>
<span class="p">()</span> <span class="o">=></span> <span class="p">{</span>
<span class="kd">const</span> <span class="nx">defaults</span> <span class="o">=</span> <span class="p">{</span>
<span class="na">event_type</span><span class="p">:</span> <span class="dl">''</span><span class="p">,</span>
<span class="na">event_date</span><span class="p">:</span> <span class="dl">''</span><span class="p">,</span>
<span class="na">title</span><span class="p">:</span> <span class="dl">''</span><span class="p">,</span>
<span class="na">speaker</span><span class="p">:</span> <span class="dl">''</span><span class="p">,</span>
<span class="na">host</span><span class="p">:</span> <span class="dl">''</span><span class="p">,</span>
<span class="na">published</span><span class="p">:</span> <span class="kc">false</span><span class="p">,</span>
<span class="p">};</span>
<span class="kd">const</span> <span class="nx">currEvent</span> <span class="o">=</span> <span class="nx">id</span> <span class="p">?</span>
<span class="nx">events</span><span class="p">.</span><span class="nx">find</span><span class="p">((</span><span class="nx">e</span><span class="p">)</span> <span class="o">=></span> <span class="nx">e</span><span class="p">.</span><span class="nx">id</span> <span class="o">===</span> <span class="nb">Number</span><span class="p">(</span><span class="nx">id</span><span class="p">))</span> <span class="p">:</span>
<span class="p">{};</span>
<span class="k">return</span> <span class="p">{</span> <span class="p">...</span><span class="nx">defaults</span><span class="p">,</span> <span class="p">...</span><span class="nx">currEvent</span> <span class="p">}</span>
<span class="p">},</span>
<span class="p">[</span><span class="nx">events</span><span class="p">,</span> <span class="nx">id</span><span class="p">]</span>
<span class="p">);</span>
</code></pre></div></div>
<p>We also need to import <code class="language-plaintext highlighter-rouge">useCallback</code> at the top of the component:</p>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="nx">React</span><span class="p">,</span> <span class="p">{</span> <span class="nx">useState</span><span class="p">,</span> <span class="nx">useRef</span><span class="p">,</span> <span class="nx">useEffect</span><span class="p">,</span> <span class="nx">useCallback</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">react</span><span class="dl">'</span><span class="p">;</span>
</code></pre></div></div>
<p>The <code class="language-plaintext highlighter-rouge">useCallback</code> hook returns a memoized version of a function, which won’t change on every render unless its own dependencies (<code class="language-plaintext highlighter-rouge">events</code> and <code class="language-plaintext highlighter-rouge">id</code>) also change. This is exactly what we want.</p>
<p>You can read more about this here: <a href="https://reactjs.org/docs/hooks-faq.html#is-it-safe-to-omit-functions-from-the-list-of-dependencies">Is it safe to omit functions from the list of dependencies?</a></p>
<h2 id="adding-some-form-tweaks">Adding Some Form Tweaks</h2>
<p>Next, let’s add a <em>Cancel</em> button to the form (in case the user changes their mind whilst editing or creating an event). We’ll also change the title of the form to reflect which action they are performing. And while we’re at it, we’ll improve the validation for our date field — at the moment it just checks if the user has entered a value.</p>
<p>In the <code class="language-plaintext highlighter-rouge"><EventForm></code> component:</p>
<div class="language-jsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">useParams</span><span class="p">,</span> <span class="nx">Link</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">react-router-dom</span><span class="dl">'</span><span class="p">;</span>
<span class="kd">const</span> <span class="nx">EventForm</span> <span class="o">=</span> <span class="p">({</span> <span class="nx">events</span><span class="p">,</span> <span class="nx">onSave</span> <span class="p">})</span> <span class="o">=></span> <span class="p">{</span>
<span class="p">...</span>
<span class="kd">const</span> <span class="nx">cancelURL</span> <span class="o">=</span> <span class="nx">event</span><span class="p">.</span><span class="nx">id</span> <span class="p">?</span> <span class="s2">`/events/</span><span class="p">${</span><span class="nx">event</span><span class="p">.</span><span class="nx">id</span><span class="p">}</span><span class="s2">`</span> <span class="p">:</span> <span class="dl">'</span><span class="s1">/events</span><span class="dl">'</span><span class="p">;</span>
<span class="kd">const</span> <span class="nx">title</span> <span class="o">=</span> <span class="nx">event</span><span class="p">.</span><span class="nx">id</span> <span class="p">?</span> <span class="s2">`</span><span class="p">${</span><span class="nx">event</span><span class="p">.</span><span class="nx">event_date</span><span class="p">}</span><span class="s2"> - </span><span class="p">${</span><span class="nx">event</span><span class="p">.</span><span class="nx">event_type</span><span class="p">}</span><span class="s2">`</span> <span class="p">:</span> <span class="dl">'</span><span class="s1">New Event</span><span class="dl">'</span><span class="p">;</span>
<span class="k">return</span> <span class="p">(</span>
<span class="p"><</span><span class="nt">div</span><span class="p">></span>
<span class="p"><</span><span class="nt">h2</span><span class="p">></span><span class="si">{</span><span class="nx">title</span><span class="si">}</span><span class="p"></</span><span class="nt">h2</span><span class="p">></span>
<span class="si">{</span><span class="nx">renderErrors</span><span class="p">()</span><span class="si">}</span>
<span class="p"><</span><span class="nt">form</span> <span class="na">className</span><span class="p">=</span><span class="s">"eventForm"</span> <span class="na">onSubmit</span><span class="p">=</span><span class="si">{</span><span class="nx">handleSubmit</span><span class="si">}</span><span class="p">></span>
...
<span class="p"><</span><span class="nt">div</span> <span class="na">className</span><span class="p">=</span><span class="s">"form-actions"</span><span class="p">></span>
<span class="p"><</span><span class="nt">button</span> <span class="na">type</span><span class="p">=</span><span class="s">"submit"</span><span class="p">></span>Save<span class="p"></</span><span class="nt">button</span><span class="p">></span>
<span class="p"><</span><span class="nc">Link</span> <span class="na">to</span><span class="p">=</span><span class="si">{</span><span class="nx">cancelURL</span><span class="si">}</span><span class="p">></span>Cancel<span class="p"></</span><span class="nc">Link</span><span class="p">></span>
<span class="p"></</span><span class="nt">div</span><span class="p">></span>
<span class="p"></</span><span class="nt">form</span><span class="p">></span>
<span class="p"></</span><span class="nt">div</span><span class="p">></span>
<span class="p">);</span>
<span class="p">};</span>
</code></pre></div></div>
<p>And the date validation in <code class="language-plaintext highlighter-rouge">helpers.js</code>:</p>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">isValidDate</span> <span class="o">=</span> <span class="nx">dateObj</span> <span class="o">=></span> <span class="o">!</span><span class="nb">Number</span><span class="p">.</span><span class="nb">isNaN</span><span class="p">(</span><span class="nb">Date</span><span class="p">.</span><span class="nx">parse</span><span class="p">(</span><span class="nx">dateObj</span><span class="p">));</span>
<span class="k">export</span> <span class="kd">const</span> <span class="nx">validateEvent</span> <span class="o">=</span> <span class="p">(</span><span class="nx">event</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="p">...</span>
<span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">isValidDate</span><span class="p">(</span><span class="nx">event</span><span class="p">.</span><span class="nx">event_date</span><span class="p">))</span> <span class="p">{</span>
<span class="nx">errors</span><span class="p">.</span><span class="nx">event_date</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">You must enter a valid date</span><span class="dl">'</span><span class="p">;</span>
<span class="p">}</span>
<span class="p">...</span>
<span class="k">return</span> <span class="nx">errors</span><span class="p">;</span>
<span class="p">};</span>
</code></pre></div></div>
<p>Finally, let’s make the header a link, that returns us to the main view. In the <code class="language-plaintext highlighter-rouge"><Header></code> component:</p>
<div class="language-jsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">Link</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">react-router-dom</span><span class="dl">'</span><span class="p">;</span>
<span class="kd">const</span> <span class="nx">Header</span> <span class="o">=</span> <span class="p">()</span> <span class="o">=></span> <span class="p">(</span>
<span class="p"><</span><span class="nt">header</span><span class="p">></span>
<span class="p"><</span><span class="nc">Link</span> <span class="na">to</span><span class="p">=</span><span class="s">'/events/'</span><span class="p">></span>
<span class="p"><</span><span class="nt">h1</span><span class="p">></span>Event Manager<span class="p"></</span><span class="nt">h1</span><span class="p">></span>
<span class="p"></</span><span class="nc">Link</span><span class="p">></span>
<span class="p"></</span><span class="nt">header</span><span class="p">></span>
<span class="p">);</span>
</code></pre></div></div>
<h2 id="adding-search">Adding Search</h2>
<p>It’d be nice to add search functionality to the events list. Luckily this is not complicated as we are holding all the events in state.</p>
<p>Let’s start off by adding a search input in our <code class="language-plaintext highlighter-rouge"><EventList></code> component:</p>
<div class="language-jsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">EventList</span> <span class="o">=</span> <span class="p">({</span> <span class="nx">events</span> <span class="p">})</span> <span class="o">=></span> <span class="p">{</span>
<span class="p">...</span>
<span class="k">return</span> <span class="p">(</span>
<span class="p"><</span><span class="nt">section</span> <span class="na">className</span><span class="p">=</span><span class="s">"eventList"</span><span class="p">></span>
<span class="p"><</span><span class="nt">h2</span><span class="p">></span>
Events
<span class="p"><</span><span class="nc">Link</span> <span class="na">to</span><span class="p">=</span><span class="s">"/events/new"</span><span class="p">></span>New Event<span class="p"></</span><span class="nc">Link</span><span class="p">></span>
<span class="p"></</span><span class="nt">h2</span><span class="p">></span>
<span class="p"><</span><span class="nt">input</span>
<span class="na">className</span><span class="p">=</span><span class="s">"search"</span>
<span class="na">placeholder</span><span class="p">=</span><span class="s">"Search"</span>
<span class="na">type</span><span class="p">=</span><span class="s">"text"</span>
<span class="na">ref</span><span class="p">=</span><span class="si">{</span><span class="nx">searchInput</span><span class="si">}</span>
<span class="na">onKeyUp</span><span class="p">=</span><span class="si">{</span><span class="nx">updateSearchTerm</span><span class="si">}</span>
<span class="p">/></span>
<span class="p"><</span><span class="nt">ul</span><span class="p">></span><span class="si">{</span><span class="nx">renderEvents</span><span class="p">(</span><span class="nx">events</span><span class="p">)</span><span class="si">}</span><span class="p"></</span><span class="nt">ul</span><span class="p">></span>
<span class="p"></</span><span class="nt">section</span><span class="p">></span>
<span class="p">);</span>
<span class="p">};</span>
</code></pre></div></div>
<p>Notice that we have added a ref to the input element, so that we can reference it elsewhere in our component. Now let’s create that ref and declare a <code class="language-plaintext highlighter-rouge">searchTerm</code> property in state.</p>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="nx">React</span><span class="p">,</span> <span class="p">{</span> <span class="nx">useState</span><span class="p">,</span> <span class="nx">useRef</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">react</span><span class="dl">'</span><span class="p">;</span>
<span class="p">...</span>
<span class="kd">const</span> <span class="nx">EventList</span> <span class="o">=</span> <span class="p">({</span> <span class="nx">events</span> <span class="p">})</span> <span class="o">=></span> <span class="p">{</span>
<span class="kd">const</span> <span class="p">[</span><span class="nx">searchTerm</span><span class="p">,</span> <span class="nx">setSearchTerm</span><span class="p">]</span> <span class="o">=</span> <span class="nx">useState</span><span class="p">(</span><span class="dl">''</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">searchInput</span> <span class="o">=</span> <span class="nx">useRef</span><span class="p">(</span><span class="kc">null</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">updateSearchTerm</span> <span class="o">=</span> <span class="p">()</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">setSearchTerm</span><span class="p">(</span><span class="nx">searchInput</span><span class="p">.</span><span class="nx">current</span><span class="p">.</span><span class="nx">value</span><span class="p">);</span>
<span class="p">};</span>
<span class="p">...</span>
<span class="p">}</span>
</code></pre></div></div>
<p>We’re also creating a <code class="language-plaintext highlighter-rouge">updateSearchTerm</code> method which will be called every time a key press is registered on the search field.</p>
<p>The list of events is rendered in the <code class="language-plaintext highlighter-rouge">renderEvents</code> method. Let’s apply a filter to our event list, so that only events matching the search criteria are displayed. Replace the entire <code class="language-plaintext highlighter-rouge">renderEvents</code> function with the following:</p>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">renderEvents</span> <span class="o">=</span> <span class="p">(</span><span class="nx">eventArray</span><span class="p">)</span> <span class="o">=></span>
<span class="nx">eventArray</span>
<span class="p">.</span><span class="nx">filter</span><span class="p">((</span><span class="nx">el</span><span class="p">)</span> <span class="o">=></span> <span class="nx">matchSearchTerm</span><span class="p">(</span><span class="nx">el</span><span class="p">))</span>
<span class="p">.</span><span class="nx">sort</span><span class="p">((</span><span class="nx">a</span><span class="p">,</span> <span class="nx">b</span><span class="p">)</span> <span class="o">=></span> <span class="k">new</span> <span class="nb">Date</span><span class="p">(</span><span class="nx">b</span><span class="p">.</span><span class="nx">event_date</span><span class="p">)</span> <span class="o">-</span> <span class="k">new</span> <span class="nb">Date</span><span class="p">(</span><span class="nx">a</span><span class="p">.</span><span class="nx">event_date</span><span class="p">))</span>
<span class="p">.</span><span class="nx">map</span><span class="p">((</span><span class="nx">event</span><span class="p">)</span> <span class="o">=></span> <span class="p">(</span>
<span class="o"><</span><span class="nx">li</span> <span class="nx">key</span><span class="o">=</span><span class="p">{</span><span class="nx">event</span><span class="p">.</span><span class="nx">id</span><span class="p">}</span><span class="o">></span>
<span class="o"><</span><span class="nx">NavLink</span> <span class="nx">to</span><span class="o">=</span><span class="p">{</span><span class="s2">`/events/</span><span class="p">${</span><span class="nx">event</span><span class="p">.</span><span class="nx">id</span><span class="p">}</span><span class="s2">`</span><span class="p">}</span><span class="o">></span>
<span class="p">{</span><span class="nx">event</span><span class="p">.</span><span class="nx">event_date</span><span class="p">}</span>
<span class="p">{</span><span class="dl">'</span><span class="s1"> - </span><span class="dl">'</span><span class="p">}</span>
<span class="p">{</span><span class="nx">event</span><span class="p">.</span><span class="nx">event_type</span><span class="p">}</span>
<span class="o"><</span><span class="sr">/NavLink</span><span class="err">>
</span> <span class="o"><</span><span class="sr">/li</span><span class="err">>
</span> <span class="p">));</span>
</code></pre></div></div>
<p>And finally, we need the <code class="language-plaintext highlighter-rouge">matchSearchTerm</code> method:</p>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">EventList</span> <span class="o">=</span> <span class="p">({</span> <span class="nx">events</span> <span class="p">})</span> <span class="o">=></span> <span class="p">{</span>
<span class="p">...</span>
<span class="kd">const</span> <span class="nx">matchSearchTerm</span> <span class="o">=</span> <span class="p">(</span><span class="nx">obj</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="kd">const</span> <span class="p">{</span> <span class="nx">id</span><span class="p">,</span> <span class="nx">published</span><span class="p">,</span> <span class="nx">created_at</span><span class="p">,</span> <span class="nx">updated_at</span><span class="p">,</span> <span class="p">...</span><span class="nx">rest</span> <span class="p">}</span> <span class="o">=</span> <span class="nx">obj</span><span class="p">;</span>
<span class="k">return</span> <span class="nb">Object</span><span class="p">.</span><span class="nx">values</span><span class="p">(</span><span class="nx">rest</span><span class="p">).</span><span class="nx">some</span><span class="p">(</span>
<span class="p">(</span><span class="nx">value</span><span class="p">)</span> <span class="o">=></span> <span class="nx">value</span><span class="p">.</span><span class="nx">toLowerCase</span><span class="p">().</span><span class="nx">indexOf</span><span class="p">(</span><span class="nx">searchTerm</span><span class="p">.</span><span class="nx">toLowerCase</span><span class="p">())</span> <span class="o">></span> <span class="o">-</span><span class="mi">1</span>
<span class="p">);</span>
<span class="p">};</span>
<span class="kd">const</span> <span class="nx">renderEvents</span> <span class="o">=</span> <span class="p">(</span><span class="nx">eventArray</span><span class="p">)</span> <span class="o">=></span> <span class="p">...</span> <span class="p">;</span>
<span class="k">return</span> <span class="p">(</span> <span class="p">...</span> <span class="p">);</span>
<span class="p">};</span>
</code></pre></div></div>
<p>Here, we are excluding some database fields that were returned by the original Ajax call, but which we are not interested in filtering by. Then, we are returning only those events which contain our search term in any of the relevant fields.</p>
<p>Et voilà! We have rudimentary search.</p>
<p><img src="https://res.cloudinary.com/hibbard/image/upload/v1648451762/event-manager-hooks/13-event-manager-with-search-functionality.png" alt="Event Manager - Search functionality" /></p>
<h2 id="adding-a-404-component">Adding a 404 Component</h2>
<p>The last thing we will do is add a component to render when an event is not found. This might be useful if a user has bookmarked an event which has since been deleted.</p>
<p>First, create the new component:</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">touch </span>app/javascript/components/EventNotFound.js
</code></pre></div></div>
<p>And add the content. Nothing exciting here:</p>
<div class="language-jsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="nx">React</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">react</span><span class="dl">'</span><span class="p">;</span>
<span class="kd">const</span> <span class="nx">EventNotFound</span> <span class="o">=</span> <span class="p">()</span> <span class="o">=></span> <span class="p"><</span><span class="nt">p</span><span class="p">></span>Event not found!<span class="p"></</span><span class="nt">p</span><span class="p">>;</span>
<span class="k">export</span> <span class="k">default</span> <span class="nx">EventNotFound</span><span class="p">;</span>
</code></pre></div></div>
<p>Now we need to update the <code class="language-plaintext highlighter-rouge"><Event></code> component:</p>
<div class="language-jsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="nx">EventNotFound</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">./EventNotFound</span><span class="dl">'</span><span class="p">;</span>
<span class="kd">const</span> <span class="nx">Event</span> <span class="o">=</span> <span class="p">({</span> <span class="nx">events</span><span class="p">,</span> <span class="nx">onDelete</span> <span class="p">})</span> <span class="o">=></span> <span class="p">{</span>
<span class="p">...</span>
<span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">event</span><span class="p">)</span> <span class="k">return</span> <span class="p"><</span><span class="nc">EventNotFound</span> <span class="p">/>;</span>
<span class="k">return</span> <span class="p">(</span> <span class="p">...</span> <span class="p">);</span>
<span class="p">};</span>
</code></pre></div></div>
<p>And finally, the <code class="language-plaintext highlighter-rouge"><EventForm></code> component:</p>
<div class="language-jsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="nx">EventNotFound</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">./EventNotFound</span><span class="dl">'</span><span class="p">;</span>
<span class="kd">const</span> <span class="nx">EventForm</span> <span class="o">=</span> <span class="p">({</span> <span class="nx">events</span><span class="p">,</span> <span class="nx">onSave</span> <span class="p">})</span> <span class="o">=></span> <span class="p">{</span>
<span class="p">...</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">id</span> <span class="o">&&</span> <span class="o">!</span><span class="nx">event</span><span class="p">.</span><span class="nx">id</span><span class="p">)</span> <span class="k">return</span> <span class="p"><</span><span class="nc">EventNotFound</span> <span class="p">/>;</span>
<span class="k">return</span> <span class="p">(</span> <span class="p">...</span> <span class="p">);</span>
<span class="p">};</span>
</code></pre></div></div>
<p>Now if the user attempts to view or edit a non-existent event, they will be shown a 404 component.</p>
<h2 id="conclusion">Conclusion</h2>
<p>And that’s everything. Well done if you’ve made it up to here. You should now have a fully functioning React/Rails CRUD app, as well as a reasonable overview of all of the moving parts involved in building something like this. I hope this tutorial has left you with a good jumping off point for creating a similar SPA of your own.</p>
<p>If you’re looking to continue honing your skills, there are a several further steps you can consider. You could, for example <a href="https://devcenter.heroku.com/categories/deployment">deploy the app to Heroku</a>, add authentication, or port the backend to use Node.</p>
<p>Either way, I’d be glad to hear your comments in the discussion below, and don’t forget, <a href="https://github.com/jameshibbard/react-rails-crud-app">the full source code for this tutorial is available on GitHub</a>.</p>James HibbardMost web applications need to persist data in one form or other. When working with a server-side language, this is normally a straightforward task. However when you add a front-end JavaScript framework to the mix, things start to get a bit trickier. In this tutorial I am going to demonstrate how to build a JSON API using Ruby on Rails and then code a fully-functional React frontend to interact with the API. The app we’ll be building is an event manager, which will let you create and manage a list of academic events. The app will showcase basic CRUD functionality and will add a couple of extra features, such as a datepicker and search.How to Prevent the .xsession-errors File From Growing Too Large2022-02-06T00:00:00+00:002022-02-06T00:00:00+00:00https://hibbard.eu/xsession-errors-cron<p>Sometimes, without warning, your <code class="language-plaintext highlighter-rouge">.xsession-errors</code> file will grow to an enormous size. This can be inconvenient at best, or crash your operating system at worst.</p>
<p>In this post I explain how you can set up a cron job to automatically watch the file and get notified if it exceeds a certain size.</p>
<!--more-->
<p>My operating system of choice is Linux Mint, but with a little tweaking this should work on most distros.</p>
<p>If you’d like to skip straight to the solution, click <a href="#solution">here</a>.</p>
<h2 id="what-is-this-file-and-why-does-it-get-so-huge">What Is This File and Why Does It Get So Huge?</h2>
<p>Many Linux distributions use something called the <a href="https://en.wikipedia.org/wiki/X_Window_System">X Window System</a> (also known as X11, or just X) to display graphical user interfaces. The <code class="language-plaintext highlighter-rouge">.xsession-errors</code> file is where X logs errors that occur within any of its client processes. These processes are regular GUI programs that you launch, such as a media player, image viewer, or file explorer.</p>
<p>The <code class="language-plaintext highlighter-rouge">.xsession-errors</code> file is located in your home directory. The dot at the beginning of the file name denotes that it is hidden by default, so you will have to make sure you can view hidden files (e.g. by pressing <kbd>Ctrl</kbd> + <kbd>H</kbd> in a file explorer) before accessing it.</p>
<p>The reason that it can grow so big, is that a large amount of applications can write to it and occasionally, one of these applications will go a little haywire and dump the same entry thousands of times over.</p>
<p>For example, this happened to me recently, when my <code class="language-plaintext highlighter-rouge">.xsession-errors</code> file grew to over 300GB and was packed full of entries like this:</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">[</span>00007f600009b310] vdpau_chroma filter error: video mixer attributes failure: An invalid handle value was provided.
<span class="o">[</span>00007f600009b310] vdpau_chroma filter error: video mixer rendering failure: An invalid handle value was provided.
<span class="o">[</span>00007f600009b310] vdpau_chroma filter error: video mixer features failure: An invalid handle value was provided.
</code></pre></div></div>
<p>It turns out this was <a href="https://unix.stackexchange.com/questions/561565/i-dont-know-what-is-producing-the-gigabytes-of-error-in-syslog">being caused by VLC</a>.</p>
<p>Every time you reboot your system, the current <code class="language-plaintext highlighter-rouge">.xsession-errors</code> file is renamed to <code class="language-plaintext highlighter-rouge">.xsession-errors.old</code> and an empty <code class="language-plaintext highlighter-rouge">.xsession-errors</code> file will be created. However, if you don’t often reboot your computer, this isn’t very helpful and other measures are called for.</p>
<h2 id="how-to-automatically-watch-the-size-of-the-xsession-errors-file">How to Automatically Watch the Size of the <code class="language-plaintext highlighter-rouge">.xsession-errors</code> File</h2>
<p>There are various posts around the internet that recommend truncating this file whenever it gets too big, or disabling writes on it altogether. And while that’s fine, I would prefer to get notified instead (via a system notification), so that I can check which application is misbehaving and address the root of the problem.</p>
<p>Doing anything automatically in regular intervals on a Linux system sounds like a job for <a href="https://en.wikipedia.org/wiki/Cron">cron</a>, so let’s look at that first.</p>
<h3 id="setting-up-a-cron-job">Setting up a Cron Job</h3>
<p>Cron is a command-line utility and you can use it to schedule jobs (commands or shell scripts) to run periodically at fixed times, dates, or intervals. You can access it by typing <code class="language-plaintext highlighter-rouge">crontab -e</code> into a terminal, which will then open the crontab file for the current user in an editor (you will be asked to select an editor if you are running this for the first time).</p>
<p><img class="shadow" alt="The crontab file open in the nano editor" src="https://res.cloudinary.com/hibbard/image/upload/v1644226027/xsession-errors/crontab.png" /></p>
<p>Enter the following line at the bottom of the file:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>* * * * * XDG_RUNTIME_DIR=/run/user/$(id -u) notify-send 'Hey' 'Hello, World!'
</code></pre></div></div>
<p>Save the file and exit. You should now recieve a “Hello, World!” system notification every minute. Nice, huh?</p>
<p><img class="shadow" alt="Test notification reading 'Hello, World!'" src="https://res.cloudinary.com/hibbard/image/upload/v1644226757/xsession-errors/test-notification.png" /></p>
<p>Satisfy yourself that this command works, then let’s take a look at what’s going on.</p>
<p>The above line breaks down as follows:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><frequency> <command-to-execute>
</code></pre></div></div>
<p>The frequency part is <code class="language-plaintext highlighter-rouge">* * * * *</code>. As we can read in the screenshot above, to define the time you can provide concrete values for minute, hour, day of the month, month, and day of the week. Or we can use <code class="language-plaintext highlighter-rouge">*</code> in these fields for ‘any’. An asterisk in every field means run the given command every minute.</p>
<blockquote>
<p>If you’d like to run a command at a different interval, you can use <a href="https://cron.help/">Cron Helper</a> to figure out the correct syntax.</p>
</blockquote>
<p>The command part is <code class="language-plaintext highlighter-rouge">notify-send 'Hey' 'Hello, World!'</code>. This uses the current desktop environment’s notification system to create a notification and is called thus: <code class="language-plaintext highlighter-rouge">notify-send 'Title' 'Message'</code>. You can also run this command straight from the command line to see it in action.</p>
<p>To <a href="https://stackoverflow.com/questions/16519673/cron-with-notify-send">use this command with cron</a> however, we need to specify a <code class="language-plaintext highlighter-rouge">XDG_RUNTIME_DIR</code> environment variable. This tells <code class="language-plaintext highlighter-rouge">notify-send</code> where to find a user-specific directory in which to store small temporary files.</p>
<p>The value we set the <code class="language-plaintext highlighter-rouge">XDG_RUNTIME_DIR</code> variable to is <code class="language-plaintext highlighter-rouge">/run/user/$(id -u)</code>, where <code class="language-plaintext highlighter-rouge">$(id -u)</code> returns the current user id as a number. This usually starts at 1000, meaning the whole path will be something like <code class="language-plaintext highlighter-rouge">/run/user/1000</code>.</p>
<p>Note that the <code class="language-plaintext highlighter-rouge">$( ... )</code> syntax is used for command substitution, which allows the output of a command to replace the command itself.</p>
<h3 id="checking-the-size-of-xsession-errors">Checking the Size of <code class="language-plaintext highlighter-rouge">.xsession-errors</code></h3>
<p>The next thing to look at is how to check the size of the <code class="language-plaintext highlighter-rouge">.xsession-errors</code> file.</p>
<p>We can do that with the disk usage command:</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">du</span> <span class="nt">-k</span> ~/.xsession-errors
</code></pre></div></div>
<p>The <code class="language-plaintext highlighter-rouge">-k</code> switch ensures that the output is in kilobytes. This will output something like:</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>240 /home/jim/.xsession-errors
</code></pre></div></div>
<p>As we are only interested in the first number (not the path), we can grab this using the <code class="language-plaintext highlighter-rouge">awk</code> command:</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">du</span> <span class="nt">-k</span> ~/.xsession-errors | <span class="nb">awk</span> <span class="s1">'{ print $1 }'</span>
</code></pre></div></div>
<p>This outputs the size of <code class="language-plaintext highlighter-rouge">.xsession-errors</code> in kilobytes:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>240
</code></pre></div></div>
<p>Finally, we can use the <code class="language-plaintext highlighter-rouge">test</code> command to check whether the file size is greater than a certain threshold. For example 1MB:</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">test</span> <span class="si">$(</span><span class="nb">du</span> <span class="nt">-k</span> ~/.xsession-errors | <span class="nb">awk</span> <span class="s1">'{ print $1 }'</span><span class="si">)</span> <span class="nt">-gt</span> 1024
</code></pre></div></div>
<p>Notice that once again we are using the command substitution syntax (<code class="language-plaintext highlighter-rouge">$( ... )</code>), to allow the output of both commands to replace the commands themselves.</p>
<p>This gives us something like this:</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">test </span>240 <span class="nt">-gt</span> 1024
</code></pre></div></div>
<p>The <code class="language-plaintext highlighter-rouge">test</code> command provides no output, but returns an exit status of 0 for “true” (test successful) and 1 for “false” (test failed). We can use this to our advantage in the next section.</p>
<p>You can also check this using the <code class="language-plaintext highlighter-rouge">$?</code> variable which represents the exit status of the previous command.</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">test </span>1 <span class="nt">-gt</span> 2<span class="p">;</span> <span class="nb">echo</span> <span class="nv">$?</span>
1
</code></pre></div></div>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">test </span>1 <span class="nt">-gt</span> 0<span class="p">;</span> <span class="nb">echo</span> <span class="nv">$?</span>
0
</code></pre></div></div>
<h3 id="only-trigger-notification-if-file-size-exceeded">Only Trigger Notification if File Size Exceeded</h3>
<p>The final piece of the puzzle is to ensure that our notification is only triggered if the size of <code class="language-plaintext highlighter-rouge">.xsession-errors</code> is above a certain threshold. We can do this with the <code class="language-plaintext highlighter-rouge">&&</code> operator, like so:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><frequency> <command-1> && <command-2>
</code></pre></div></div>
<p>The right side of <code class="language-plaintext highlighter-rouge">&&</code> will only be evaluated if the exit status of the left side is zero (i.e. true).</p>
<p><span id="solution">This gives us the following as our final solution:</span></p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">*</span>/15 <span class="k">*</span> <span class="k">*</span> <span class="k">*</span> <span class="k">*</span> <span class="o">[</span> <span class="si">$(</span><span class="nb">du</span> <span class="nt">-k</span> .xsession-errors | <span class="nb">awk</span> <span class="s1">'{ print $1 }'</span><span class="si">)</span> <span class="nt">-gt</span> 1024 <span class="o">]</span> <span class="o">&&</span> <span class="nv">XDG_RUNTIME_DIR</span><span class="o">=</span>/run/user/<span class="si">$(</span><span class="nb">id</span> <span class="nt">-u</span><span class="si">)</span> notify-send <span class="s1">'Hey'</span> <span class="s1">'.xsession errors is getting too big!'</span>
</code></pre></div></div>
<p>Note that I am using square brackets as an alias for the <code class="language-plaintext highlighter-rouge">test</code> command and that I have set the cron job to run every 15 minutes (which is plenty). It is also not necessary to specify the complete path of the <code class="language-plaintext highlighter-rouge">.xsession-errors</code> file, as the cron job will be run in the user’s home directory.</p>
<p>If the file size exceeds whichever threshold you have specified, a notification will be triggered every 15 minutes until the problem is addressed.</p>
<p>For added convenience, I have also <a href="https://www.sitepoint.com/zsh-commands-plugins-aliases-tools/#15customaliasestoboostyourproductivity">set up an alias in my shell</a>, so that when I type <code class="language-plaintext highlighter-rouge">x</code>, the <code class="language-plaintext highlighter-rouge">.xsession-errors</code> file is opened in my editor.</p>
<h2 id="conclusion">Conclusion</h2>
<p>In this post I have shown how to set up a cron job to automatically monitor the size of the <code class="language-plaintext highlighter-rouge">.xsession-errors</code> file and to trigger a notification if the file size becomes too big. I hope people find this useful. If you have any questions or comments, let me know below.</p>James HibbardSometimes, without warning, your .xsession-errors file will grow to an enormous size. This can be inconvenient at best, or crash your operating system at worst. In this post I explain how you can set up a cron job to automatically watch the file and get notified if it exceeds a certain size.How to Send Tweets With a JavaScript GitHub Action2021-04-05T00:00:00+00:002021-04-05T00:00:00+00:00https://hibbard.eu/tweet-javascript-github-action<p>GitHub actions is a feature which allows developers to construct workflows that run in response to various GitHub events. You can use them, for example, to run tests when a new pull request is received, post new issues to Slack, or publish a package to npm.</p>
<p>Previously, this kind of setup would have required a service such as Travis, or Circle CI. Actions however, are an official GitHub offering and give you first-class support for your automation needs.</p>
<p>In this article, I’m going to demonstrate how to create a GitHub action using JavaScript. This will post a tweet to Twitter every time a pull request is merged.</p>
<!--more-->
<h2 id="pre-requisites">Pre-requisites</h2>
<p>To follow along with this tutorial you will need a recent version of Node installed. If you don’t have Node on your system yet, head to the <a href="https://nodejs.org/en/download/">official Node download page</a> and grab the relevant binaries. Alternatively, consider using a <a href="https://www.sitepoint.com/quick-tip-multiple-versions-node-nvm/">version manager</a>, which allows you to install multiple versions of Node and switch between them at will.</p>
<p>You’ll also need <a href="https://git-scm.com/downloads">git</a> installed and a <a href="https://twitter.com">Twitter</a> account.</p>
<p>Please note that the code for this article is <a href="https://github.com/jameshibbard/github-tweet-action">available on GitHub</a>.</p>
<h2 id="creating-the-github-action">Creating the GitHub Action</h2>
<p>Our GitHub action will live in its own repository, as this will make it easier for other people to use. This repository will contain all of the code necessary to programatically send a tweet.</p>
<p>Let’s start off by creating a <a href="https://github.com/new">new repository on GitHub</a>. Fill out a name (I’ll call mine <code class="language-plaintext highlighter-rouge">github-tweet-action</code>) and a description, then hit the <em>Create repository</em> button. This will create the repository on GitHub and display a bunch of commands to help us initialize our project via the command line. We’ll do this in the next step.</p>
<p>Next, make a new folder with the same name as the GitHub repository on your computer:</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">mkdir </span>github-tweet-action
<span class="nb">cd </span>github-tweet-action
</code></pre></div></div>
<p>Inside the project, run the commands suggested by GitHub in the previous step. They should look something like this:</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">echo</span> <span class="s2">"# github-tweet-action"</span> <span class="o">>></span> README.md
git init
git add README.md
git commit <span class="nt">-m</span> <span class="s2">"first commit"</span>
git branch <span class="nt">-M</span> main
git remote add origin git@github.com:jameshibbard/github-tweet-action.git
git push <span class="nt">-u</span> origin main
</code></pre></div></div>
<p>Now when you refresh your browser, you should see your shiny new README file on GitHub.</p>
<h2 id="tweeting-using-a-twitter-client">Tweeting Using a Twitter Client</h2>
<p>The library we will be using to send tweets is <a href="https://www.npmjs.com/package/twitter">Twitter for Node.js</a>. It hasn’t had an update for a couple of years, but it seems stable enough for our purposes (based primarily on the fact that it has 38k+ weekly downloads on npm). It also makes it easy to add an image to our tweets, which is a nice bonus.</p>
<p>Let’s initilaize an npm project in our <code class="language-plaintext highlighter-rouge">github-twitter-action</code> folder and install the dependency.</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>npm init <span class="nt">-y</span>
npm i twitter
</code></pre></div></div>
<p>Next, make a <code class="language-plaintext highlighter-rouge">src</code> directory in the project root and inside that directory create an <code class="language-plaintext highlighter-rouge">index.js</code> file.</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">mkdir </span>src
<span class="nb">touch </span>src/index.js
</code></pre></div></div>
<p>In the <code class="language-plaintext highlighter-rouge">index.js</code> file, add the following:</p>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">Twitter</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="dl">'</span><span class="s1">twitter</span><span class="dl">'</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">client</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">Twitter</span><span class="p">({</span>
<span class="na">consumer_key</span><span class="p">:</span> <span class="dl">''</span><span class="p">,</span>
<span class="na">consumer_secret</span><span class="p">:</span> <span class="dl">''</span><span class="p">,</span>
<span class="na">access_token_key</span><span class="p">:</span> <span class="dl">''</span><span class="p">,</span>
<span class="na">access_token_secret</span><span class="p">:</span> <span class="dl">''</span><span class="p">,</span>
<span class="p">});</span>
<span class="nx">client</span><span class="p">.</span><span class="nx">post</span><span class="p">(</span>
<span class="dl">'</span><span class="s1">statuses/update</span><span class="dl">'</span><span class="p">,</span>
<span class="p">{</span> <span class="na">status</span><span class="p">:</span> <span class="dl">'</span><span class="s1">I tweeted from Node.js!</span><span class="dl">'</span> <span class="p">},</span>
<span class="p">(</span><span class="nx">error</span><span class="p">,</span> <span class="nx">tweet</span><span class="p">,</span> <span class="nx">response</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">error</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">error</span><span class="p">(</span><span class="nx">error</span><span class="p">);</span>
<span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">tweet</span><span class="p">);</span>
<span class="p">}</span>
<span class="p">},</span>
<span class="p">);</span>
</code></pre></div></div>
<p>As you can see, we’re going to need some valid Twitter developer credentials to interact with their API. Let’s get those now.</p>
<h3 id="creating-a-twitter-app">Creating a Twitter App</h3>
<p>In order to proceed with this next section, you’ll be required to have a Twitter developer account and have activated the <a href="https://developer.twitter.com/en/portal/opt-in">new developer portal experience</a>.</p>
<p>If you don’t have a developer account, head <a href="https://developer.twitter.com/en/apply-for-access">here</a> and apply. Please be aware that as part of this process, you will be asked to add a phone number and set an email address for your account. If this doesn’t sit well with you, you can alter these once your account is approved.</p>
<p>When applying, you will also have to submit detailed information about your intended use of Twitter APIs.</p>
<p>Here are the questions I was asked and how I answered:</p>
<ul>
<li><strong>Primary reason for using Twitter Developer Tools</strong>: Doing something else</li>
<li><strong>Country</strong>: Germany</li>
<li><strong>What would you like us to call you</strong>: James Hibbard</li>
<li><strong>How you will use the Twitter API</strong>: To publish tweets using a GitHub action I am writing.</li>
<li><strong>Do you intend to analyze Tweets</strong>: No</li>
<li><strong>How will Twitter data be displayed to users of your solution</strong>: No twitter data will be displayed</li>
<li><strong>Will your product, service, or analysis make Twitter content or derived information available to a government entity?</strong> No</li>
</ul>
<p>After submitting your answers, you will receive an email to verify your developer account. This can reportedly take as long as one or two days, but in my case it was much shorter.</p>
<p>Finally, you’ll be able to create an app in the <a href="https://developer.twitter.com/en/portal/apps/new">developer portal</a>.</p>
<p><img src="https://res.cloudinary.com/hibbard/image/upload/v1616873507/github-action/create-twitter-app.png" alt="Create a new Twitter app" class="shadow" /></p>
<p>Enter a name for your app (I called mine <em>Mes Bottes</em>), you’ll then be taken to a screen displaying an API key, an API secret key and a Bearer token. Make a note of the API key and the API secret.</p>
<p>Below the keys and tokens, you’ll notice an <em>App settings</em> button. Click this.</p>
<p>On the next page select the <em>Keys and tokens</em> tab at the top of the screen. You’ll then see a <em>Generate</em> button, which will allow you to generate an access token and secret. Click this.</p>
<p>This will open a popup containing the access token and the acess token secret. Make a note of these values, then click <em>Yes, I saved them</em> to shut the window.</p>
<h3 id="sending-a-tweet-programatically">Sending a Tweet Programatically</h3>
<p>So as to be able to interact with the Twitter API, add your newly obtained credentials to <code class="language-plaintext highlighter-rouge">index.js</code>:</p>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">client</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">Twitter</span><span class="p">({</span>
<span class="na">consumer_key</span><span class="p">:</span> <span class="dl">'</span><span class="s1"><Your API key here></span><span class="dl">'</span><span class="p">,</span>
<span class="na">consumer_secret</span><span class="p">:</span> <span class="dl">'</span><span class="s1"><Your API key secret here></span><span class="dl">'</span><span class="p">,</span>
<span class="na">access_token_key</span><span class="p">:</span> <span class="dl">'</span><span class="s1"><Your API key secret here></span><span class="dl">'</span><span class="p">,</span>
<span class="na">access_token_secret</span><span class="p">:</span> <span class="dl">'</span><span class="s1"><Your access token secret here></span><span class="dl">'</span><span class="p">,</span>
<span class="p">});</span>
</code></pre></div></div>
<p>Now, if you hop into your terminal and run the script with <code class="language-plaintext highlighter-rouge">node src/index.js</code>, it should post a tweet to your Twitter account 🎉</p>
<p><em>Note: it’s normally a bad idea to hardcode access tokens into a web app. If we were doing anything more than this one-off test, it would be better to keep them in a <code class="language-plaintext highlighter-rouge">.env</code> file and load them into your app using a package such as <a href="https://www.npmjs.com/package/dotenv">dotenv</a>.</em></p>
<h2 id="adding-the-tweet-action">Adding the Tweet Action</h2>
<p>Now we can post tweets via Twitter’s API, the next thing we need to do is set up the GitHub action. Let’s start by installing the <a href="https://www.npmjs.com/package/@actions/core">@actions/core</a> package, which contains functions for setting results, logging and registering secrets.</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>npm i @actions/core @actions/github
</code></pre></div></div>
<p>Now we need to update <code class="language-plaintext highlighter-rouge">index.js</code> to make use of this package:</p>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">Twitter</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="dl">'</span><span class="s1">twitter</span><span class="dl">'</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">core</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="dl">'</span><span class="s1">@actions/core</span><span class="dl">'</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">client</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">Twitter</span><span class="p">({</span>
<span class="na">consumer_key</span><span class="p">:</span> <span class="nx">core</span><span class="p">.</span><span class="nx">getInput</span><span class="p">(</span><span class="dl">'</span><span class="s1">consumer-key</span><span class="dl">'</span><span class="p">),</span>
<span class="na">consumer_secret</span><span class="p">:</span> <span class="nx">core</span><span class="p">.</span><span class="nx">getInput</span><span class="p">(</span><span class="dl">'</span><span class="s1">consumer-secret</span><span class="dl">'</span><span class="p">),</span>
<span class="na">access_token_key</span><span class="p">:</span> <span class="nx">core</span><span class="p">.</span><span class="nx">getInput</span><span class="p">(</span><span class="dl">'</span><span class="s1">access-token</span><span class="dl">'</span><span class="p">),</span>
<span class="na">access_token_secret</span><span class="p">:</span> <span class="nx">core</span><span class="p">.</span><span class="nx">getInput</span><span class="p">(</span><span class="dl">'</span><span class="s1">access-token-secret</span><span class="dl">'</span><span class="p">),</span>
<span class="p">});</span>
<span class="k">async</span> <span class="kd">function</span> <span class="nx">run</span><span class="p">()</span> <span class="p">{</span>
<span class="k">try</span> <span class="p">{</span>
<span class="nx">client</span><span class="p">.</span><span class="nx">post</span><span class="p">(</span>
<span class="dl">'</span><span class="s1">statuses/update</span><span class="dl">'</span><span class="p">,</span>
<span class="p">{</span>
<span class="na">status</span><span class="p">:</span> <span class="dl">'</span><span class="s1">This tweet was posted when a pull request was merged!</span><span class="dl">'</span><span class="p">,</span>
<span class="p">},</span>
<span class="p">(</span><span class="nx">error</span><span class="p">,</span> <span class="nx">tweet</span><span class="p">,</span> <span class="nx">response</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">error</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">error</span><span class="p">(</span><span class="nx">error</span><span class="p">);</span>
<span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">tweet</span><span class="p">);</span>
<span class="p">}</span>
<span class="p">},</span>
<span class="p">);</span>
<span class="p">}</span> <span class="k">catch</span> <span class="p">(</span><span class="nx">error</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">core</span><span class="p">.</span><span class="nx">setFailed</span><span class="p">(</span><span class="nx">error</span><span class="p">.</span><span class="nx">message</span><span class="p">);</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="nx">run</span><span class="p">();</span>
</code></pre></div></div>
<p>Notice how we are using <code class="language-plaintext highlighter-rouge">core.getInput</code> to access our Twitter credentials, which we are going to store as secrets in GitHub.</p>
<h3 id="the-actionyml-file">The <code class="language-plaintext highlighter-rouge">action.yml</code> File</h3>
<p>An action is defined in an <code class="language-plaintext highlighter-rouge">action.yml</code> file, which specifies a bunch of metadata, such as name, description, inputs, outputs, entrypoint, and what version of Node to run on. You can read more about the metadata syntax for GitHub Actions <a href="https://docs.github.com/en/actions/creating-actions/metadata-syntax-for-github-actions">here</a>.</p>
<p>Start by creating the <code class="language-plaintext highlighter-rouge">action.yml</code> file in the root of your project:</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">touch </span>action.yml
</code></pre></div></div>
<p>Then add the following content:</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">name</span><span class="pi">:</span> <span class="s1">'</span><span class="s">GitHub</span><span class="nv"> </span><span class="s">Tweet</span><span class="nv"> </span><span class="s">Action'</span>
<span class="na">description</span><span class="pi">:</span> <span class="s1">'</span><span class="s">Send</span><span class="nv"> </span><span class="s">tweet</span><span class="nv"> </span><span class="s">on</span><span class="nv"> </span><span class="s">PR</span><span class="nv"> </span><span class="s">merge'</span>
<span class="na">author</span><span class="pi">:</span> <span class="s1">'</span><span class="s">James</span><span class="nv"> </span><span class="s">Hibbard'</span>
<span class="na">inputs</span><span class="pi">:</span>
<span class="na">consumer-key</span><span class="pi">:</span>
<span class="na">description</span><span class="pi">:</span> <span class="pi">></span>
<span class="s">Consumer API key, available in the "Keys and tokens"</span>
<span class="s">section of your application in the Twitter Developer site.</span>
<span class="na">consumer-secret</span><span class="pi">:</span>
<span class="na">description</span><span class="pi">:</span> <span class="pi">></span>
<span class="s">Consumer API secret key, available in the "Keys and tokens"</span>
<span class="s">section of your application in the Twitter Developer site.</span>
<span class="na">access-token</span><span class="pi">:</span>
<span class="na">description</span><span class="pi">:</span> <span class="pi">></span>
<span class="s">Application access token, available in the "Keys and tokens"</span>
<span class="s">section of your application in the Twitter Developer site.</span>
<span class="na">access-token-secret</span><span class="pi">:</span>
<span class="na">description</span><span class="pi">:</span> <span class="pi">></span>
<span class="s">Application access token secret, available in the "Keys and tokens"</span>
<span class="s">section of your application in the Twitter Developer site.</span>
<span class="na">runs</span><span class="pi">:</span>
<span class="na">using</span><span class="pi">:</span> <span class="s1">'</span><span class="s">node12'</span>
<span class="na">main</span><span class="pi">:</span> <span class="s1">'</span><span class="s">dist/index.js'</span>
</code></pre></div></div>
<p>Please note, YAML is very strict with indentation, so make sure you copy the above correctly.</p>
<p>There are a few things to be aware of here – <code class="language-plaintext highlighter-rouge">name</code>, <code class="language-plaintext highlighter-rouge">description</code> and <code class="language-plaintext highlighter-rouge">author</code> are hopefully self explanatory. After that we have an <code class="language-plaintext highlighter-rouge">inputs</code> key, which specifies data that the action expects to use during runtime. The <code class="language-plaintext highlighter-rouge">runs</code> key specifies the version of Node we will use, as well as the entrypoint (<code class="language-plaintext highlighter-rouge">main</code>) which tells the runner, which file to execute.</p>
<p><em>At the time of writing, the latest LTS version of Node <a href="https://github.com/actions/runner/issues/772">doesn’t seem to be supported</a>, so we have dropped back to using v12.</em></p>
<h3 id="packaging-the-action-with-ncc">Packaging the Action With ncc</h3>
<p>Now you might be wondering how we’re going to fit our entire action into one file. Didn’t we just install a Twitter client that pulled in a whole load of dependencies?</p>
<p>Quite right, we did and if you take a peek in the <code class="language-plaintext highlighter-rouge">node_modules</code> folder, there are a whole lot of further modules in there.</p>
<p>Luckily, we can use a tool called <a href="https://npmjs.com/@vercel/ncc">ncc</a> to compile everything we need into a single file.</p>
<p>Let’s start off by creating a <code class="language-plaintext highlighter-rouge">.gitignore</code> file and adding <code class="language-plaintext highlighter-rouge">node_modules</code> to it:</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">touch</span> .gitignore
<span class="nb">echo</span> <span class="s2">"node_modules"</span> <span class="o">>></span> .gitignore
</code></pre></div></div>
<p>Then install ncc as a <code class="language-plaintext highlighter-rouge">devDependency</code>:</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>npm i <span class="nt">-D</span> @vercel/ncc
</code></pre></div></div>
<p>Create an npm script in your <code class="language-plaintext highlighter-rouge">package.json</code> file to compile the project to a single file:</p>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nl">"scripts"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nl">"build"</span><span class="p">:</span><span class="w"> </span><span class="s2">"ncc build src/index.js"</span><span class="w">
</span><span class="p">}</span><span class="err">,</span><span class="w">
</span></code></pre></div></div>
<p>And finally, run the script:</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>npm run build
</code></pre></div></div>
<p>This will create a new <code class="language-plaintext highlighter-rouge">dist</code> folder containing an <code class="language-plaintext highlighter-rouge">index.js</code> file, which contains all of the code of our action.</p>
<p>Once you are satisfied that everything has worked, commit your changes and push them to GitHub.</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git add all
git commit <span class="nt">-m</span> <span class="s2">"Added tweet action"</span>
git push
</code></pre></div></div>
<h2 id="creating-a-repository-to-use-the-tweet-action">Creating a Repository to Use the Tweet Action</h2>
<p>Ok, so that’s our action up and running, now we need to create a second repository which will use it.</p>
<p>To do this, head to GitHub and create another <a href="https://github.com/new">new repository</a>. Fill out a name (I’ll call mine <code class="language-plaintext highlighter-rouge">tweets</code>) and a description, then hit the <em>Create repository</em> button.</p>
<p>We might as well go ahead and make an initial commit and push everything to GitHub:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>mkdir tweets
cd tweets
echo "# Tweets" >> README.md
git init
git add README.md
git commit -m "first commit"
git branch -M main
git remote add origin git@github.com:jameshibbard/tweets.git
git push -u origin main
</code></pre></div></div>
<p>Now let’s add a workflow, which will make use of our tweet action.</p>
<p>To do this, we will need to create a hidden <code class="language-plaintext highlighter-rouge">.github</code> folder, which contains a <code class="language-plaintext highlighter-rouge">workflows</code> directory. Please remember, this is all taking place in our second repository (<code class="language-plaintext highlighter-rouge">tweets</code>) – the one which will use the action.</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">mkdir</span> <span class="nt">-p</span> .github/workflows
</code></pre></div></div>
<p>The workflow will live in a <code class="language-plaintext highlighter-rouge">main.yml</code> file inside the <code class="language-plaintext highlighter-rouge">workflows</code> directory. Let’s create that, too:</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">touch</span> .github/workflows/main.yml
</code></pre></div></div>
<p>And add the following content:</p>
<div class="language-yml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">on</span><span class="pi">:</span>
<span class="na">push</span><span class="pi">:</span>
<span class="na">branches</span><span class="pi">:</span>
<span class="pi">-</span> <span class="s">main</span>
<span class="na">jobs</span><span class="pi">:</span>
<span class="na">post_tweet_job</span><span class="pi">:</span>
<span class="na">runs-on</span><span class="pi">:</span> <span class="s">ubuntu-latest</span>
<span class="na">name</span><span class="pi">:</span> <span class="s">Post tweet to Twitter</span>
<span class="na">steps</span><span class="pi">:</span>
<span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Tweets contents of PR</span>
<span class="na">uses</span><span class="pi">:</span> <span class="s">jameshibbard/github-tweet-action@main</span>
<span class="na">with</span><span class="pi">:</span>
<span class="na">consumer-key</span><span class="pi">:</span> <span class="s">${{ secrets.TWITTER_CONSUMER_KEY }}</span>
<span class="na">consumer-secret</span><span class="pi">:</span> <span class="s">${{ secrets.TWITTER_CONSUMER_SECRET }}</span>
<span class="na">access-token</span><span class="pi">:</span> <span class="s">${{ secrets.TWITTER_ACCESS_TOKEN_KEY }}</span>
<span class="na">access-token-secret</span><span class="pi">:</span> <span class="s">${{ secrets.TWITTER_ACCESS_TOKEN_SECRET }}</span>
</code></pre></div></div>
<p>Here, we are specifying that the workflow should run whenever we push to the repo’s master branch, or merge a PR into master. Unfortunately, there isn’t an explicit <code class="language-plaintext highlighter-rouge">on-pullrequest-merge</code> event we can hook into, but this comes pretty close. You can read more about events that trigger workflows <a href="https://docs.github.com/en/actions/reference/events-that-trigger-workflows">here</a>.</p>
<p>Next comes the job that the workflow needs to run. We only have one here, but you could specify as many as you like. The job runs on the latest version of Ubuntu (v20.04) and consists of one step. Again, you could have multiple steps here, but we need only the one.</p>
<p>The name of the step can be anything you fancy, but what is important is that you point the <code class="language-plaintext highlighter-rouge">uses</code> key at the <code class="language-plaintext highlighter-rouge">main</code> branch of the repo that contains the action we made previously.</p>
<p>Finally, we are specifying where the action can find our Twitter credentials, so that it can interact with Twitter’s API. We’ll be adding these to GitHub soon.</p>
<p>Commit your changes and push them to GitHub:</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git add <span class="nt">--all</span>
git commit <span class="nt">-m</span> <span class="s2">"Added workflow to be run on PR merge"</span>
git push
</code></pre></div></div>
<p>As we have specified that the workflow should be run when we push to our main branch, this push should already trigger a run.</p>
<p>In your browser, navigate to the repository which will be using the GitHub action as part of a workflow. For me, this is <a href="https://github.com/jameshibbard/tweets">https://github.com/jameshibbard/tweets</a></p>
<p>Click on the <em>Actions</em> tab and you should see a notice that the workflow run was unsuccessful.</p>
<p><img src="https://res.cloudinary.com/hibbard/image/upload/v1617619202/github-action/tweets-workflow-unsuccessful.png" alt="Tweets workflow unsuccessful" class="shadow" /></p>
<p>This makes sense, as we haven’t added our Twitter credentials to the repo yet. Let’s do that now.</p>
<h3 id="adding-our-twitter-credentials-to-github">Adding Our Twitter Credentials to GitHub</h3>
<p>We’re going to store our Twitter keys as secrets. Click on <em>Settings</em> > <em>Secrets</em> > <em>Actions</em>, then click the <em>New repository secret</em> button.</p>
<p>You should see the following screen:</p>
<p><img src="https://res.cloudinary.com/hibbard/image/upload/v1617544533/github-action/github-secrets.png" alt="GitHub secrets page" class="shadow" /></p>
<p>Create four new secrets as follows:</p>
<ul>
<li><code class="language-plaintext highlighter-rouge">TWITTER_CONSUMER_KEY</code>: <Your Twitter API key></li>
<li><code class="language-plaintext highlighter-rouge">TWITTER_CONSUMER_SECRET</code>: <Your Twitter API secret></li>
<li><code class="language-plaintext highlighter-rouge">TWITTER_ACCESS_TOKEN_KEY</code>: <Your access token></li>
<li><code class="language-plaintext highlighter-rouge">TWITTER_ACCESS_TOKEN_SECRET</code>: <Your access token secret></li>
</ul>
<p>When you’re done, head back to the main page of the repository.</p>
<h2 id="testing-the-workflow">Testing the Workflow</h2>
<p>At this point, if we have wired everything up correctly, the workflow should kick in when we merge a pull request and post a tweet to Twitter.</p>
<p>Let’s give it a test.</p>
<p>Run the following commands in the folder containing the repository that is using the GitHub action. For me this is the <code class="language-plaintext highlighter-rouge">tweets</code> folder.</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git checkout -b pr-1
echo "\n© 2021" >> README.md
git add README.md
git commit -m "Updated README"
git push origin pr-1
</code></pre></div></div>
<p>This will create a new branch called <em>pr-1</em>. On this branch we then update the README file, before committing our change and pushing the branch to the remote repository on GitHub.</p>
<p>If you now look at the repo on GitHub, you should see a notification informing you of the new branch and a big green <em>Compare and pull request</em> button. Click this and create a new pull request.</p>
<p>You should now see a 1 by the <em>Pull requests</em> tab on the horizontal nav bar. Make sure this tab is selected, then click the <em>Merge pull request</em> button, followed by <em>Confirm merge</em>. This should give you a notification that the pull request was successfully merged and closed.</p>
<p>Finally, click the <em>Actions</em> tab and you should see that the workflow is running for a second time. This time it should be successful.</p>
<p><img src="https://res.cloudinary.com/hibbard/image/upload/v1617619201/github-action/tweets-workflow-successful.png" alt="Tweets workflow successful" class="shadow" /></p>
<p>We now have a working GitHub action!</p>
<h2 id="tweeting-the-pull-request-message">Tweeting the Pull Request Message</h2>
<p>Currently, when we merge a pull request, a tweet is posted that reads:</p>
<blockquote>
<p>This tweet was posted when a pull request was merged!</p>
</blockquote>
<p>In this final section, I’m going to demonstrate how to include the PR title in the tweet. This means we will end up with something such as:</p>
<blockquote>
<p>A new PR was merged into jameshibbard/tweets: Updated README</p>
</blockquote>
<p>To obtain this information we’re going to need a further dependency:</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>npm i @actions/github
</code></pre></div></div>
<p>The <a href="https://www.npmjs.com/package/@actions/github">@actions/github</a> package returns an authenticated Octokit REST client and access to GitHub Actions contexts. We can use these contexts to extract the information we need.</p>
<p>In the repository containing your action, update <code class="language-plaintext highlighter-rouge">src/index.js</code> as follows:</p>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">Twitter</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="dl">'</span><span class="s1">twitter</span><span class="dl">'</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">core</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="dl">'</span><span class="s1">@actions/core</span><span class="dl">'</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">github</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="dl">'</span><span class="s1">@actions/github</span><span class="dl">'</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">client</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">Twitter</span><span class="p">({</span>
<span class="na">consumer_key</span><span class="p">:</span> <span class="nx">core</span><span class="p">.</span><span class="nx">getInput</span><span class="p">(</span><span class="dl">'</span><span class="s1">consumer-key</span><span class="dl">'</span><span class="p">),</span>
<span class="na">consumer_secret</span><span class="p">:</span> <span class="nx">core</span><span class="p">.</span><span class="nx">getInput</span><span class="p">(</span><span class="dl">'</span><span class="s1">consumer-secret</span><span class="dl">'</span><span class="p">),</span>
<span class="na">access_token_key</span><span class="p">:</span> <span class="nx">core</span><span class="p">.</span><span class="nx">getInput</span><span class="p">(</span><span class="dl">'</span><span class="s1">access-token</span><span class="dl">'</span><span class="p">),</span>
<span class="na">access_token_secret</span><span class="p">:</span> <span class="nx">core</span><span class="p">.</span><span class="nx">getInput</span><span class="p">(</span><span class="dl">'</span><span class="s1">access-token-secret</span><span class="dl">'</span><span class="p">),</span>
<span class="p">});</span>
<span class="kd">const</span> <span class="nx">repoName</span> <span class="o">=</span> <span class="nx">github</span><span class="p">.</span><span class="nx">context</span><span class="p">.</span><span class="nx">payload</span><span class="p">.</span><span class="nx">repository</span><span class="p">.</span><span class="nx">full_name</span><span class="p">;</span>
<span class="kd">const</span> <span class="p">{</span> <span class="nx">message</span> <span class="p">}</span> <span class="o">=</span> <span class="nx">github</span><span class="p">.</span><span class="nx">context</span><span class="p">.</span><span class="nx">payload</span><span class="p">.</span><span class="nx">commits</span><span class="p">[</span><span class="mi">0</span><span class="p">];</span>
<span class="k">async</span> <span class="kd">function</span> <span class="nx">run</span><span class="p">()</span> <span class="p">{</span>
<span class="k">try</span> <span class="p">{</span>
<span class="nx">client</span><span class="p">.</span><span class="nx">post</span><span class="p">(</span>
<span class="dl">'</span><span class="s1">statuses/update</span><span class="dl">'</span><span class="p">,</span>
<span class="p">{</span>
<span class="na">status</span><span class="p">:</span> <span class="s2">`A new PR was merged into </span><span class="p">${</span><span class="nx">repoName</span><span class="p">}</span><span class="s2">: </span><span class="p">${</span><span class="nx">message</span><span class="p">}</span><span class="s2">`</span><span class="p">,</span>
<span class="p">},</span>
<span class="p">(</span><span class="nx">error</span><span class="p">,</span> <span class="nx">tweet</span><span class="p">,</span> <span class="nx">response</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">error</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">error</span><span class="p">(</span><span class="nx">error</span><span class="p">);</span>
<span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">tweet</span><span class="p">);</span>
<span class="p">}</span>
<span class="p">},</span>
<span class="p">);</span>
<span class="p">}</span> <span class="k">catch</span> <span class="p">(</span><span class="nx">error</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">core</span><span class="p">.</span><span class="nx">setFailed</span><span class="p">(</span><span class="nx">error</span><span class="p">.</span><span class="nx">message</span><span class="p">);</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="nx">run</span><span class="p">();</span>
</code></pre></div></div>
<p>As you can see, the <code class="language-plaintext highlighter-rouge">@actions/github</code> package gives us access to a <code class="language-plaintext highlighter-rouge">context</code> object, which will contain different information depending on the event that triggered the workflow.</p>
<p>In our case, this was a <a href="https://docs.github.com/en/developers/webhooks-and-events/webhook-events-and-payloads#push">push event</a>, and we can grab the information we need from <code class="language-plaintext highlighter-rouge">repository.full_name</code> and <code class="language-plaintext highlighter-rouge">commits</code>.</p>
<p>Next, re-run the <code class="language-plaintext highlighter-rouge">build</code> task to rebundle our action into <code class="language-plaintext highlighter-rouge">dist/index.js</code>:</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>npm run build
</code></pre></div></div>
<p>And push the changes to GitHub:</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git add <span class="nt">--all</span>
git commit <span class="nt">-m</span> <span class="s2">"Updated tweet action"</span>
git push
</code></pre></div></div>
<p>Now, when you make a new PR to the repo using the tweet, then merge that PR, you should see a more informative tweet pop up on your Twitter timeline.</p>
<h2 id="conclusion">Conclusion</h2>
<p>I hope you have enjoyed this tutorial. You should now have a decent handle on GitHub actions and know both how to create them, as well as how to use them.</p>
<p>If you have any questions or comments, hit me up in the comments below.</p>
<p>And don’t forget that you can find the code on GitHub: <a href="https://github.com/jameshibbard/github-tweet-action">https://github.com/jameshibbard/github-tweet-action</a></p>James HibbardGitHub actions is a feature which allows developers to construct workflows that run in response to various GitHub events. You can use them, for example, to run tests when a new pull request is received, post new issues to Slack, or publish a package to npm. Previously, this kind of setup would have required a service such as Travis, or Circle CI. Actions however, are an official GitHub offering and give you first-class support for your automation needs. In this article, I’m going to demonstrate how to create a GitHub action using JavaScript. This will post a tweet to Twitter every time a pull request is merged.Authentication with Devise and cancancan in Rails2020-06-19T00:00:00+00:002020-06-19T00:00:00+00:00https://hibbard.eu/authentication-with-devise-and-cancancan-in-rails<p>This is a tutorial on how to set up authentication (verifying who you are) and authorization (what you are permitted to do) using Ruby 2.7, Rails 6.0.3 and two popular Ruby gems: <a href="https://github.com/heartcombo/devise" title="Flexible authentication solution for Rails with Warden">Devise</a> and <a href="https://github.com/CanCanCommunity/cancancan" title="Continuation of CanCan, the authorization Gem for Ruby on Rails">cancancan</a>.</p>
<p>The code for this tutorial is on GitHub: <a href="https://github.com/hibbard-eu/authentication-with-devise-and-cancancan" title="Project code">https://github.com/hibbard-eu/authentication-with-devise-and-cancancan</a></p>
<!--more-->
<h2 id="the-scenario">The Scenario</h2>
<p>The app we’ll be coding is a store. In order for people to use the store, they’ll need to register an account. The store will also have sellers (otherwise it would be a rubbish store) and an admin.</p>
<p>This means we’ll need the following resources: <code class="language-plaintext highlighter-rouge">Item</code>, <code class="language-plaintext highlighter-rouge">User</code>, <code class="language-plaintext highlighter-rouge">Role</code>.</p>
<p>Here’s a UML diagram showing how they relate to one another:</p>
<p><img src="https://res.cloudinary.com/hibbard/image/upload/v1593028898/rails-devise-cancancan/uml-diagram.png" alt="UML diagram depicting the assosciiations between the three resources in the app" title="UML diagram depicting the assosciiations between the three resources in the app" /></p>
<p>Note that users can have a maximum of one role. The permissions for each of these users will break down as follows:</p>
<ul>
<li>Unregistered users are redirected to the sign up page</li>
<li>Registered users: can view items</li>
<li>Sellers: can view items, create items, as well as update and destroy any items that belong to them</li>
<li>Admin: can perform any CRUD operation on any resource</li>
</ul>
<p>So let’s get started.</p>
<h2 id="generating-the-project-files">Generating the Project Files</h2>
<p>Let’s start by creating a new project.</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>rails new store
</code></pre></div></div>
<p>In its current version, Rails uses <a href="https://github.com/rails/webpacker">Webpacker</a> as its default JavaScript compiler. Webpacker expects us to have both <a href="https://nodejs.org/en/">Node.js</a> and the <a href="https://yarnpkg.com/">Yarn package manager</a> installed. If you don’t have Node installed already, I would recommend using a <a href="https://www.sitepoint.com/quick-tip-multiple-versions-node-nvm/">version manager</a> which will let you switch between versions with ease and which also negates certain permission errors.</p>
<p>For this tutorial, I am using the current LTS version of Node (12.18.1) and the latest version of Yarn (1.22.4). Yarn should be installed globally using <code class="language-plaintext highlighter-rouge">npm -i g yarn</code>.</p>
<p>Once the project has been created, we can change into the <code class="language-plaintext highlighter-rouge">store</code> directory and remove the following line from our <code class="language-plaintext highlighter-rouge">Gemfile</code>:</p>
<div class="language-diff highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gd">- gem 'jbuilder', '~> 2.7'
</span></code></pre></div></div>
<p><a href="https://github.com/rails/jbuilder">Jbuilder</a> is used for generating and rendering JSON responses for API requests in Rails. We won’t be needing this functionality.</p>
<p>Next, let’s use the <a href="https://www.rubyguides.com/2020/03/rails-scaffolding/">scaffold generator</a> to create our project files:</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>rails g scaffold user name:string role:belongs_to
rails g scaffold role name:string description:string
rails g scaffold item name:string description:text <span class="s1">'price:decimal{5,2}'</span>, user:belongs_to
</code></pre></div></div>
<p>If you’re wondering what <code class="language-plaintext highlighter-rouge">price:decimal{5,2}</code> does, it adds the following to the migration file:</p>
<div class="language-rb highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">.</span><span class="nf">decimal</span> <span class="ss">:price</span><span class="p">,</span> <span class="ss">precision: </span><span class="mi">5</span><span class="p">,</span> <span class="ss">scale: </span><span class="mi">2</span>
</code></pre></div></div>
<p>A decimal with a precision of 5 and a scale of 2 can range from -999.99 to 999.99.</p>
<p>Finally, run the migrations with the following command.</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>rake db:migrate
</code></pre></div></div>
<h2 id="editing-the-boilerplate">Editing the Boilerplate</h2>
<p>We’ll need to tailor the files that Rails has generated to suit our needs.</p>
<p>Start by removing the following line from the top of the <code class="language-plaintext highlighter-rouge">index</code> and <code class="language-plaintext highlighter-rouge">show</code> view templates for the <code class="language-plaintext highlighter-rouge">User</code>, <code class="language-plaintext highlighter-rouge">Role</code> and <code class="language-plaintext highlighter-rouge">Item</code> resources.</p>
<div class="language-diff highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gd">- <p id="notice"><%= notice %></p>
</span></code></pre></div></div>
<p>In <code class="language-plaintext highlighter-rouge">items/index.html.erb</code> change “User” to “Seller” and <code class="language-plaintext highlighter-rouge">item.user_id</code> to <code class="language-plaintext highlighter-rouge">item.user.name</code>.</p>
<p>In <code class="language-plaintext highlighter-rouge">items/show.html.erb</code> change “User” to “Seller” and <code class="language-plaintext highlighter-rouge">@item.user_id</code> to <code class="language-plaintext highlighter-rouge">@item.user.name</code>.</p>
<p>In <code class="language-plaintext highlighter-rouge">items/_form.html.erb</code> remove the complete <code class="language-plaintext highlighter-rouge">user_id</code> field (including the surrounding div tags).</p>
<div class="language-diff highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gd">- <div class="field">
- <%= form.label :user_id %>
- <%= form.text_field :user_id %>
- </div>
</span></code></pre></div></div>
<p>In <code class="language-plaintext highlighter-rouge">users/index.html.erb</code> change <code class="language-plaintext highlighter-rouge">user.role_id</code> to <code class="language-plaintext highlighter-rouge">user.role.name</code></p>
<p>In <code class="language-plaintext highlighter-rouge">users/show.html.erb</code> change <code class="language-plaintext highlighter-rouge">@user.role_id</code> to <code class="language-plaintext highlighter-rouge">@user.role.name</code></p>
<p>In <code class="language-plaintext highlighter-rouge">users/_form.html.erb</code> change <code class="language-plaintext highlighter-rouge"><%= form.text_field :role_id %></code> to:</p>
<div class="language-erb highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp"><%=</span> <span class="n">collection_select</span><span class="p">(</span>
<span class="ss">:user</span><span class="p">,</span> <span class="ss">:role_id</span><span class="p">,</span> <span class="no">Role</span><span class="p">.</span><span class="nf">all</span><span class="p">,</span> <span class="ss">:id</span><span class="p">,</span> <span class="ss">:name</span><span class="p">,</span> <span class="p">{</span> <span class="ss">prompt: </span><span class="kp">true</span> <span class="p">}</span>
<span class="p">)</span> <span class="cp">%></span>
</code></pre></div></div>
<p>At this point if you start up Puma (<code class="language-plaintext highlighter-rouge">rails s</code>) and visit <a href="http://localhost:3000/items">http://localhost:3000/items</a>, <a href="http://localhost:3000/roles">http://localhost:3000/roles</a>, or <a href="http://localhost:3000/users">http://localhost:3000/users</a>, you can see that our basic scaffolding is working (albeit without any data)</p>
<p><img src="https://res.cloudinary.com/hibbard/image/upload/v1593075472/rails-devise-cancancan/basic-scaffold.png" alt="Create a new item" title="Rails CRUD interface to create a new item" /></p>
<p>Everything is now setup to implement the authentication logic.</p>
<h2 id="authentication-with-devise">Authentication with Devise</h2>
<p><a href="https://github.com/heartcombo/devise">Devise</a> is a full-featured authentication solution for Rails based on Warden. One of the things I like about it most (aside from the ease of use) is that it is built on a modularity concept. This makes it easy to include only those features you need in your application.</p>
<p>To get started with Devise, add it to our project’s Gemfile:</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>bundle add devise
</code></pre></div></div>
<p>Then run Devise’s installation generator:</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>rails g devise:install
</code></pre></div></div>
<p>This command generates a couple of files: an initializer and a locale file that contains all of the messages that Devise needs to display.</p>
<p>As per the post installation message, we’ll need to make a couple of alterations to our config files.</p>
<p>Add the following line to the bottom of <code class="language-plaintext highlighter-rouge">config/environments/development.rb</code>:</p>
<div class="language-rb highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">config</span><span class="p">.</span><span class="nf">action_mailer</span><span class="p">.</span><span class="nf">default_url_options</span> <span class="o">=</span> <span class="p">{</span> <span class="ss">host: </span><span class="s1">'localhost:3000'</span> <span class="p">}</span>
</code></pre></div></div>
<p>And set a root route in <code class="language-plaintext highlighter-rouge">/config/routes.rb</code></p>
<div class="language-rb highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">root</span> <span class="ss">to: </span><span class="s1">'items#index'</span>
</code></pre></div></div>
<p>We’re going to use the <code class="language-plaintext highlighter-rouge">User</code> model to handle authentication and Devise provides a generator for doing just that:</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>rails g devise User
rake db:migrate
</code></pre></div></div>
<p>Devise won’t override our current <code class="language-plaintext highlighter-rouge">User</code> model – it will simply add a bunch of attributes to it.</p>
<p>Edit the <code class="language-plaintext highlighter-rouge">/app/views/layouts/application.html.erb</code> to include the following immediately before the <code class="language-plaintext highlighter-rouge"><%= yield %></code>:</p>
<div class="language-erb highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp"><%</span> <span class="k">if</span> <span class="n">user_signed_in?</span> <span class="cp">%></span>
Signed in as <span class="cp"><%=</span> <span class="n">current_user</span><span class="p">.</span><span class="nf">email</span> <span class="cp">%></span>. Not you?
<span class="cp"><%=</span> <span class="n">link_to</span> <span class="s2">"Edit profile"</span><span class="p">,</span> <span class="n">edit_user_registration_path</span> <span class="cp">%></span>
<span class="cp"><%=</span> <span class="n">link_to</span> <span class="s2">"Sign out"</span><span class="p">,</span> <span class="n">destroy_user_session_path</span><span class="p">,</span> <span class="ss">method: :delete</span> <span class="cp">%></span>
<span class="cp"><%</span> <span class="k">else</span> <span class="cp">%></span>
<span class="cp"><%=</span> <span class="n">link_to</span> <span class="s2">"Sign up"</span><span class="p">,</span> <span class="n">new_user_registration_path</span> <span class="cp">%></span> or
<span class="cp"><%=</span> <span class="n">link_to</span> <span class="s2">"sign in"</span><span class="p">,</span> <span class="n">new_user_session_path</span> <span class="cp">%></span>
<span class="cp"><%</span> <span class="k">end</span> <span class="cp">%></span>
<span class="cp"><%</span> <span class="n">flash</span><span class="p">.</span><span class="nf">each</span> <span class="k">do</span> <span class="o">|</span><span class="nb">name</span><span class="p">,</span> <span class="n">msg</span><span class="o">|</span> <span class="cp">%></span>
<span class="cp"><%=</span> <span class="n">content_tag</span> <span class="ss">:div</span><span class="p">,</span> <span class="n">msg</span><span class="p">,</span> <span class="ss">id: </span><span class="s2">"flash_</span><span class="si">#{</span><span class="nb">name</span><span class="si">}</span><span class="s2">"</span> <span class="cp">%></span>
<span class="cp"><%</span> <span class="k">end</span> <span class="cp">%></span>
</code></pre></div></div>
<p>And add the following line to the top of <code class="language-plaintext highlighter-rouge">ItemsController</code>, <code class="language-plaintext highlighter-rouge">RolesController</code> and <code class="language-plaintext highlighter-rouge">UsersController</code>:</p>
<div class="language-rb highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">before_action</span> <span class="ss">:authenticate_user!</span>
</code></pre></div></div>
<p>This will ensure that the user is logged in before being able to access any of these resources.</p>
<p>Now is the time to add some data. Alter <code class="language-plaintext highlighter-rouge">db/seeds.rb</code> thus:</p>
<div class="language-rb highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">r1</span> <span class="o">=</span> <span class="no">Role</span><span class="p">.</span><span class="nf">create</span><span class="p">({</span> <span class="ss">name: </span><span class="s1">'Regular'</span><span class="p">,</span> <span class="ss">description: </span><span class="s1">'Can read items'</span> <span class="p">})</span>
<span class="n">r2</span> <span class="o">=</span> <span class="no">Role</span><span class="p">.</span><span class="nf">create</span><span class="p">({</span> <span class="ss">name: </span><span class="s1">'Seller'</span><span class="p">,</span> <span class="ss">description: </span><span class="s1">'Can read and create items. Can update and destroy own items'</span> <span class="p">})</span>
<span class="n">r3</span> <span class="o">=</span> <span class="no">Role</span><span class="p">.</span><span class="nf">create</span><span class="p">({</span> <span class="ss">name: </span><span class="s1">'Admin'</span><span class="p">,</span> <span class="ss">description: </span><span class="s1">'Can perform any CRUD operation on any resource'</span> <span class="p">})</span>
<span class="n">u1</span> <span class="o">=</span> <span class="no">User</span><span class="p">.</span><span class="nf">create</span><span class="p">({</span> <span class="ss">name: </span><span class="s1">'Sally'</span><span class="p">,</span> <span class="ss">email: </span><span class="s1">'sally@example.com'</span><span class="p">,</span> <span class="ss">password: </span><span class="s1">'aaaaaaaa'</span><span class="p">,</span> <span class="ss">password_confirmation: </span><span class="s1">'aaaaaaaa'</span><span class="p">,</span> <span class="ss">role_id: </span><span class="n">r1</span><span class="p">.</span><span class="nf">id</span> <span class="p">})</span>
<span class="n">u2</span> <span class="o">=</span> <span class="no">User</span><span class="p">.</span><span class="nf">create</span><span class="p">({</span> <span class="ss">name: </span><span class="s1">'Sue'</span><span class="p">,</span> <span class="ss">email: </span><span class="s1">'sue@example.com'</span><span class="p">,</span> <span class="ss">password: </span><span class="s1">'aaaaaaaa'</span><span class="p">,</span> <span class="ss">password_confirmation: </span><span class="s1">'aaaaaaaa'</span><span class="p">,</span> <span class="ss">role_id: </span><span class="n">r2</span><span class="p">.</span><span class="nf">id</span> <span class="p">})</span>
<span class="n">u3</span> <span class="o">=</span> <span class="no">User</span><span class="p">.</span><span class="nf">create</span><span class="p">({</span> <span class="ss">name: </span><span class="s1">'Kev'</span><span class="p">,</span> <span class="ss">email: </span><span class="s1">'kev@example.com'</span><span class="p">,</span> <span class="ss">password: </span><span class="s1">'aaaaaaaa'</span><span class="p">,</span> <span class="ss">password_confirmation: </span><span class="s1">'aaaaaaaa'</span><span class="p">,</span> <span class="ss">role_id: </span><span class="n">r2</span><span class="p">.</span><span class="nf">id</span> <span class="p">})</span>
<span class="n">u4</span> <span class="o">=</span> <span class="no">User</span><span class="p">.</span><span class="nf">create</span><span class="p">({</span> <span class="ss">name: </span><span class="s1">'Jack'</span><span class="p">,</span> <span class="ss">email: </span><span class="s1">'jack@example.com'</span><span class="p">,</span> <span class="ss">password: </span><span class="s1">'aaaaaaaa'</span><span class="p">,</span> <span class="ss">password_confirmation: </span><span class="s1">'aaaaaaaa'</span><span class="p">,</span> <span class="ss">role_id: </span><span class="n">r3</span><span class="p">.</span><span class="nf">id</span> <span class="p">})</span>
<span class="n">i1</span> <span class="o">=</span> <span class="no">Item</span><span class="p">.</span><span class="nf">create</span><span class="p">({</span> <span class="ss">name: </span><span class="s1">'Rayban Sunglasses'</span><span class="p">,</span> <span class="ss">description: </span><span class="s1">'Stylish shades'</span><span class="p">,</span> <span class="ss">price: </span><span class="mf">99.99</span><span class="p">,</span> <span class="ss">user_id: </span><span class="n">u2</span><span class="p">.</span><span class="nf">id</span> <span class="p">})</span>
<span class="n">i2</span> <span class="o">=</span> <span class="no">Item</span><span class="p">.</span><span class="nf">create</span><span class="p">({</span> <span class="ss">name: </span><span class="s1">'Gucci watch'</span><span class="p">,</span> <span class="ss">description: </span><span class="s1">'Expensive timepiece'</span><span class="p">,</span> <span class="ss">price: </span><span class="mf">199.99</span><span class="p">,</span> <span class="ss">user_id: </span><span class="n">u2</span><span class="p">.</span><span class="nf">id</span> <span class="p">})</span>
<span class="n">i3</span> <span class="o">=</span> <span class="no">Item</span><span class="p">.</span><span class="nf">create</span><span class="p">({</span> <span class="ss">name: </span><span class="s1">'Henri Lloyd Pullover'</span><span class="p">,</span> <span class="ss">description: </span><span class="s1">'Classy knitwear'</span><span class="p">,</span> <span class="ss">price: </span><span class="mf">299.99</span><span class="p">,</span> <span class="ss">user_id: </span><span class="n">u3</span><span class="p">.</span><span class="nf">id</span> <span class="p">})</span>
<span class="n">i4</span> <span class="o">=</span> <span class="no">Item</span><span class="p">.</span><span class="nf">create</span><span class="p">({</span> <span class="ss">name: </span><span class="s1">'Porsche socks'</span><span class="p">,</span> <span class="ss">description: </span><span class="s1">'Cosy footwear'</span><span class="p">,</span> <span class="ss">price: </span><span class="mf">399.99</span><span class="p">,</span> <span class="ss">user_id: </span><span class="n">u3</span><span class="p">.</span><span class="nf">id</span> <span class="p">})</span>
</code></pre></div></div>
<p>Then run:</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>rake db:seed
</code></pre></div></div>
<p>Finally, we can restart the Rails server and log in using the email address and password of one of the users we defined in the seeds file.</p>
<p>Notice that if we are logged out and try and access any of the protected resources, we are redirected to a log in page with the message “You need to sign in or sign up before continuing.”</p>
<p><img src="https://res.cloudinary.com/hibbard/image/upload/v1593075954/rails-devise-cancancan/sign-in-with-devise.png" alt="Log in page with the message "You need to sign in or sign up before continuing."" title="Devise prevents unauthorized users from accessing resources" /></p>
<p>Exciting times, huh?</p>
<h2 id="a-bit-more-about-devise">A Bit More About Devise</h2>
<p>As I mentioned briefly above, Devise is based on a modularity concept. To expand on that, take a peek at the <code class="language-plaintext highlighter-rouge">User</code> model. You should see:</p>
<div class="language-rb highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">User</span> <span class="o"><</span> <span class="no">ApplicationRecord</span>
<span class="c1"># Include default devise modules. Others available are:</span>
<span class="c1"># :confirmable, :lockable, :timeoutable, :trackable and :omniauthable</span>
<span class="n">devise</span> <span class="ss">:database_authenticatable</span><span class="p">,</span> <span class="ss">:registerable</span><span class="p">,</span>
<span class="ss">:recoverable</span><span class="p">,</span> <span class="ss">:rememberable</span><span class="p">,</span> <span class="ss">:validatable</span>
<span class="n">belongs_to</span> <span class="ss">:role</span>
<span class="k">end</span>
</code></pre></div></div>
<p>In its default configuration, Devise comes with five of ten modules enabled.</p>
<p>You can see these in action in our app – for example <code class="language-plaintext highlighter-rouge">Rememberable</code> remembers the user’s authentication in a saved cookie (embodied by the checkbox on the login form that says “Remember me”).</p>
<p>If you didn’t require this functionality, simply comment out <code class="language-plaintext highlighter-rouge">:rememberable</code> and it won’t be included.</p>
<p>I would encourage you to take the time to read through what all of the modules do. They are listed on <a href="https://github.com/heartcombo/devise#readme" title="Devise's readme on its GitHub project page">the project’s readme</a></p>
<p>Now, to get our app working properly, we’ll going to need to make a few tweaks.</p>
<h2 id="adding-an-admin-namespace">Adding an Admin Namespace</h2>
<p>Let’s start by namespacing the CRUD interface. This is necessary as otherwise <a href="https://github.com/heartcombo/devise/wiki/How-To:-Manage-users-through-a-CRUD-interface" title="How To: Manage users through a CRUD interface">the user registration routes and user management routes can conflict</a>. Alter <code class="language-plaintext highlighter-rouge">config/routes.rb</code> like so:</p>
<div class="language-rb highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">devise_for</span> <span class="ss">:users</span>
<span class="n">scope</span> <span class="s1">'/admin'</span> <span class="k">do</span>
<span class="n">resources</span> <span class="ss">:users</span>
<span class="k">end</span>
</code></pre></div></div>
<p>Then, in the <code class="language-plaintext highlighter-rouge">User</code> model, we can add the missing association (<code class="language-plaintext highlighter-rouge">app/models/user.rb</code>):</p>
<div class="language-rb highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">has_many</span> <span class="ss">:items</span><span class="p">,</span> <span class="ss">dependent: :destroy</span>
</code></pre></div></div>
<p>We are specifying <code class="language-plaintext highlighter-rouge">dependent: :destroy</code> as if we delete a seller, it also makes sense to delete the items they are selling.</p>
<p>And in the <code class="language-plaintext highlighter-rouge">ItemsController</code>, make sure that a user is assosciated with each item before it is saved:</p>
<div class="language-rb highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">create</span>
<span class="vi">@item</span> <span class="o">=</span> <span class="no">Item</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="n">item_params</span><span class="p">)</span>
<span class="vi">@item</span><span class="p">.</span><span class="nf">user_id</span> <span class="o">=</span> <span class="n">current_user</span><span class="p">.</span><span class="nf">id</span>
<span class="o">...</span>
<span class="k">end</span>
</code></pre></div></div>
<p>Once you have restarted the server, you will be able to (kinda) manage users at <a href="http://localhost:3000/admin/users">http://localhost:3000/admin/users</a>. I write kinda, as you’ll not yet be able to create new users. We’ll get to that a little later.</p>
<h2 id="customizing-devise">Customizing Devise</h2>
<p>Now, if you click on the “sign up” or “edit profile” links, you’ll notice that the user’s name attribute is missing. It would be nice to have users be able to specify their names when registering, so let’s fix that.</p>
<p>Devise comes with many views built-in. If we would like to customize these pages, we must transfer the Devise view files into our project so we can modify them. Luckily, Devise has a generator to do just that:</p>
<div class="language-rb highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">rails</span> <span class="n">g</span> <span class="n">devise</span><span class="ss">:views</span>
</code></pre></div></div>
<p>This will copy across a bunch of templates to the <code class="language-plaintext highlighter-rouge">app/views/devise</code> directory.</p>
<p>Change into this folder, then into the <code class="language-plaintext highlighter-rouge">registrations</code> sub-folder. Locate the <code class="language-plaintext highlighter-rouge">new.html.erb</code> and <code class="language-plaintext highlighter-rouge">edit.html.erb</code> files and add the following just after <code class="language-plaintext highlighter-rouge"><%= render "devise/shared/error_messages", resource: resource %></code>:</p>
<div class="language-erb highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt"><div</span> <span class="na">class=</span><span class="s">"field"</span><span class="nt">></span>
<span class="cp"><%=</span> <span class="n">f</span><span class="p">.</span><span class="nf">label</span> <span class="ss">:name</span> <span class="cp">%></span><span class="nt"><br</span> <span class="nt">/></span>
<span class="cp"><%=</span> <span class="n">f</span><span class="p">.</span><span class="nf">text_field</span> <span class="ss">:name</span> <span class="cp">%></span>
<span class="nt"></div></span>
</code></pre></div></div>
<p>This will add the correct fields to the appropriate forms, but were you to attempt to fill them out at this point, you would notice that Devise isn’t saving your changes.</p>
<p>The reason for this is that the extra parameter you are passing to the controller (<code class="language-plaintext highlighter-rouge">name</code> in this case) needs to be expressly permitted.</p>
<p>You can do this as follows in the <code class="language-plaintext highlighter-rouge">ApplicationController</code>:</p>
<div class="language-rb highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">ApplicationController</span> <span class="o"><</span> <span class="no">ActionController</span><span class="o">::</span><span class="no">Base</span>
<span class="n">before_action</span> <span class="ss">:configure_permitted_parameters</span><span class="p">,</span> <span class="ss">if: :devise_controller?</span>
<span class="kp">protected</span>
<span class="k">def</span> <span class="nf">configure_permitted_parameters</span>
<span class="n">devise_parameter_sanitizer</span><span class="p">.</span><span class="nf">permit</span><span class="p">(</span><span class="ss">:sign_up</span><span class="p">,</span> <span class="ss">keys: </span><span class="p">[</span><span class="ss">:name</span><span class="p">])</span>
<span class="n">devise_parameter_sanitizer</span><span class="p">.</span><span class="nf">permit</span><span class="p">(</span><span class="ss">:account_update</span><span class="p">,</span> <span class="ss">keys: </span><span class="p">[</span><span class="ss">:name</span><span class="p">])</span>
<span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>
<p>Also, when a user signs up, they need to be assigned a role. We can make this default to “Regular”. And while we’re at it, we can also add some validation to make sure a user enters a name.</p>
<p>To do this, alter <code class="language-plaintext highlighter-rouge">/app/models/user.rb</code> as follows:</p>
<div class="language-rb highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">User</span> <span class="o"><</span> <span class="no">ApplicationRecord</span>
<span class="c1"># Include default devise modules. Others available are:</span>
<span class="c1"># :confirmable, :lockable, :timeoutable, :trackable and :omniauthable</span>
<span class="n">devise</span> <span class="ss">:database_authenticatable</span><span class="p">,</span> <span class="ss">:registerable</span><span class="p">,</span>
<span class="ss">:recoverable</span><span class="p">,</span> <span class="ss">:rememberable</span><span class="p">,</span> <span class="ss">:validatable</span>
<span class="n">belongs_to</span> <span class="ss">:role</span><span class="p">,</span> <span class="ss">optional: </span><span class="kp">true</span>
<span class="n">has_many</span> <span class="ss">:items</span><span class="p">,</span> <span class="ss">dependent: :destroy</span>
<span class="n">validates</span> <span class="ss">:name</span><span class="p">,</span> <span class="ss">presence: </span><span class="kp">true</span>
<span class="n">before_save</span> <span class="ss">:assign_role</span>
<span class="k">def</span> <span class="nf">assign_role</span>
<span class="nb">self</span><span class="p">.</span><span class="nf">role</span> <span class="o">=</span> <span class="no">Role</span><span class="p">.</span><span class="nf">find_by</span> <span class="ss">name: </span><span class="s1">'Regular'</span> <span class="k">if</span> <span class="n">role</span><span class="p">.</span><span class="nf">nil?</span>
<span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>
<p>Take a moment at this point to start up the app again and make sure that everything is working. It is? Good.</p>
<h2 id="creating-new-users">Creating New Users</h2>
<p>The final thing we’re going to do, is give the admin the power to create new users via the admin interface, as well as to edit any of the existing users’ attributes.</p>
<p>First off, let’s add the user’s email address to the views.</p>
<p><code class="language-plaintext highlighter-rouge">app/views/users/index.html.erb</code>:</p>
<div class="language-erb highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt"><th></span>Role<span class="nt"></th></span>
<span class="nt"><th></span>Email<span class="nt"></th></span>
<span class="nt"><th</span> <span class="na">colspan=</span><span class="s">"3"</span><span class="nt">></th></span>
</code></pre></div></div>
<div class="language-erb highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt"><td></span><span class="cp"><%=</span> <span class="n">user</span><span class="p">.</span><span class="nf">role</span><span class="p">.</span><span class="nf">name</span> <span class="cp">%></span><span class="nt"></td></span>
<span class="nt"><td></span><span class="cp"><%=</span> <span class="n">user</span><span class="p">.</span><span class="nf">email</span> <span class="cp">%></span><span class="nt"></td></span>
<span class="nt"><td></span><span class="cp"><%=</span> <span class="n">link_to</span> <span class="s1">'Show'</span><span class="p">,</span> <span class="n">user</span> <span class="cp">%></span><span class="nt"></td></span>
</code></pre></div></div>
<p><code class="language-plaintext highlighter-rouge">app/views/users/show.html.erb</code>:</p>
<div class="language-erb highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt"><p></span>
<span class="nt"><strong></span>Email:<span class="nt"></strong></span>
<span class="cp"><%=</span> <span class="vi">@user</span><span class="p">.</span><span class="nf">email</span> <span class="cp">%></span>
<span class="nt"></p></span>
</code></pre></div></div>
<p><code class="language-plaintext highlighter-rouge">app/views/users/_form.html.erb</code>:</p>
<div class="language-erb highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt"><div</span> <span class="na">class=</span><span class="s">"field"</span><span class="nt">></span>
<span class="cp"><%=</span> <span class="n">form</span><span class="p">.</span><span class="nf">label</span> <span class="ss">:email</span> <span class="cp">%></span><span class="nt"><br></span>
<span class="cp"><%=</span> <span class="n">form</span><span class="p">.</span><span class="nf">text_field</span> <span class="ss">:email</span> <span class="cp">%></span>
<span class="nt"></div></span>
</code></pre></div></div>
<p>In the form partial, we can also add a password field:</p>
<div class="language-erb highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt"><div</span> <span class="na">class=</span><span class="s">"field"</span><span class="nt">></span>
<span class="cp"><%=</span> <span class="n">form</span><span class="p">.</span><span class="nf">label</span> <span class="ss">:password</span> <span class="cp">%></span><span class="nt"><br></span>
<span class="cp"><%=</span> <span class="n">form</span><span class="p">.</span><span class="nf">password_field</span> <span class="ss">:password</span><span class="p">,</span> <span class="ss">placeholder: </span><span class="s2">"Leave blank if unchanged"</span> <span class="cp">%></span>
<span class="nt"></div></span>
</code></pre></div></div>
<p>Now things get a little complicated, so as to be able to create a new user via our admin interface (<a href="http://localhost:3000/admin/users/new">http://localhost:3000/admin/users/new</a>), we need to permit the attributes that Devise requires:</p>
<p><code class="language-plaintext highlighter-rouge">/app/controllers/users_controller.rb</code></p>
<div class="language-rb highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">user_params</span>
<span class="n">params</span><span class="p">.</span><span class="nf">require</span><span class="p">(</span><span class="ss">:user</span><span class="p">).</span><span class="nf">permit</span><span class="p">(</span>
<span class="ss">:email</span><span class="p">,</span>
<span class="ss">:password</span><span class="p">,</span>
<span class="ss">:password_confirmation</span><span class="p">,</span>
<span class="ss">:name</span><span class="p">,</span>
<span class="ss">:role_id</span>
<span class="p">)</span>
<span class="k">end</span>
</code></pre></div></div>
<p>That’s ok, but if we try and edit an existing user (e.g. <a href="http://localhost:3000/admin/users/1/edit">http://localhost:3000/admin/users/1/edit</a>), then we are prompted to set a new password every time we want to update something. This is not ideal.</p>
<p>To get around this, we can take advantage of Devise’s <a href="http://www.rubydoc.info/github/plataformatec/devise/Devise/Models/DatabaseAuthenticatable:update_without_password" title="Method: Devise::Models::DatabaseAuthenticatable#update_without_password">update_without_password</a> method.</p>
<p>Alter the update method in <code class="language-plaintext highlighter-rouge">UsersController</code> as shown (making sure to include the protected method <code class="language-plaintext highlighter-rouge">needs_password?</code>):</p>
<div class="language-rb highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">update</span>
<span class="k">if</span> <span class="n">user_params</span><span class="p">[</span><span class="ss">:password</span><span class="p">].</span><span class="nf">blank?</span>
<span class="n">user_params</span><span class="p">.</span><span class="nf">delete</span><span class="p">(</span><span class="ss">:password</span><span class="p">)</span>
<span class="n">user_params</span><span class="p">.</span><span class="nf">delete</span><span class="p">(</span><span class="ss">:password_confirmation</span><span class="p">)</span>
<span class="k">end</span>
<span class="n">successfully_updated</span> <span class="o">=</span> <span class="k">if</span> <span class="n">needs_password?</span><span class="p">(</span><span class="vi">@user</span><span class="p">,</span> <span class="n">user_params</span><span class="p">)</span>
<span class="vi">@user</span><span class="p">.</span><span class="nf">update</span><span class="p">(</span><span class="n">user_params</span><span class="p">)</span>
<span class="k">else</span>
<span class="vi">@user</span><span class="p">.</span><span class="nf">update_without_password</span><span class="p">(</span><span class="n">user_params</span><span class="p">)</span>
<span class="k">end</span>
<span class="k">if</span> <span class="n">successfully_updated</span>
<span class="n">redirect_to</span> <span class="vi">@user</span><span class="p">,</span> <span class="ss">notice: </span><span class="s1">'User was successfully updated.'</span>
<span class="k">else</span>
<span class="n">render</span> <span class="ss">:edit</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="kp">private</span>
<span class="k">def</span> <span class="nf">needs_password?</span><span class="p">(</span><span class="n">_user</span><span class="p">,</span> <span class="n">params</span><span class="p">)</span>
<span class="n">params</span><span class="p">[</span><span class="ss">:password</span><span class="p">].</span><span class="nf">present?</span>
<span class="k">end</span>
</code></pre></div></div>
<p>And with that we can now create, update and delete users.</p>
<p><img src="https://res.cloudinary.com/hibbard/image/upload/v1593076446/rails-devise-cancancan/create-user.png" alt="Creating a new user" title="Creating a new user via our admin interface" /></p>
<h2 id="enabling-the-trackable-module">Enabling the Trackable Module</h2>
<p>Before we finish looking at Devise and authentication, let’s enable the trackable module, to give us a little more information on our users.</p>
<p>In <code class="language-plaintext highlighter-rouge">/app/models/users.rb</code> alter the code like so:</p>
<div class="language-rb highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">User</span> <span class="o"><</span> <span class="no">ApplicationRecord</span>
<span class="c1"># Include default devise modules. Others available are:</span>
<span class="c1"># :confirmable, :lockable, :timeoutable and :omniauthable</span>
<span class="n">devise</span> <span class="ss">:database_authenticatable</span><span class="p">,</span> <span class="ss">:registerable</span><span class="p">,</span>
<span class="ss">:recoverable</span><span class="p">,</span> <span class="ss">:rememberable</span><span class="p">,</span> <span class="ss">:trackable</span><span class="p">,</span> <span class="ss">:validatable</span>
<span class="o">...</span>
<span class="k">end</span>
</code></pre></div></div>
<p>Create a new migration:</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>rails generate migration AddDeviseTrackableColumnsToUsers
</code></pre></div></div>
<p>This will create a new file in the <code class="language-plaintext highlighter-rouge">db/migrate</code> folder. Alter it like so:</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">AddDeviseTrackableColumnsToUsers</span> <span class="o"><</span> <span class="no">ActiveRecord</span><span class="o">::</span><span class="no">Migration</span><span class="p">[</span><span class="mf">6.0</span><span class="p">]</span>
<span class="k">def</span> <span class="nc">self</span><span class="o">.</span><span class="nf">up</span>
<span class="n">change_table</span> <span class="ss">:users</span> <span class="k">do</span> <span class="o">|</span><span class="n">t</span><span class="o">|</span>
<span class="n">t</span><span class="p">.</span><span class="nf">integer</span> <span class="ss">:sign_in_count</span><span class="p">,</span> <span class="ss">default: </span><span class="mi">0</span><span class="p">,</span> <span class="ss">null: </span><span class="kp">false</span>
<span class="n">t</span><span class="p">.</span><span class="nf">datetime</span> <span class="ss">:current_sign_in_at</span>
<span class="n">t</span><span class="p">.</span><span class="nf">datetime</span> <span class="ss">:last_sign_in_at</span>
<span class="n">t</span><span class="p">.</span><span class="nf">string</span> <span class="ss">:current_sign_in_ip</span>
<span class="n">t</span><span class="p">.</span><span class="nf">string</span> <span class="ss">:last_sign_in_ip</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>
<p>Then run the migration with <code class="language-plaintext highlighter-rouge">rake db:migrate</code>. As you can see from the migration file, this will add several columns to the user table.</p>
<p>Now, add the following to <code class="language-plaintext highlighter-rouge">/app/views/users/show.html.erb</code>:</p>
<div class="language-erb highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt"><p></span>
<span class="nt"><strong></span>Joined on:<span class="nt"></strong></span>
<span class="cp"><%=</span> <span class="vi">@joined_on</span> <span class="cp">%></span>
<span class="nt"></p></span>
<span class="nt"><p></span>
<span class="nt"><strong></span>Last logged in on:<span class="nt"></strong></span>
<span class="cp"><%=</span> <span class="vi">@last_login</span> <span class="cp">%></span>
<span class="nt"></p></span>
<span class="nt"><p></span>
<span class="nt"><strong></span>No. times logged in:<span class="nt"></strong></span>
<span class="cp"><%=</span> <span class="vi">@user</span><span class="p">.</span><span class="nf">sign_in_count</span> <span class="cp">%></span>
<span class="nt"></p></span>
</code></pre></div></div>
<p>And the logic to the <code class="language-plaintext highlighter-rouge">UsersController</code>:</p>
<div class="language-rb highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">show</span>
<span class="vi">@joined_on</span> <span class="o">=</span> <span class="vi">@user</span><span class="p">.</span><span class="nf">created_at</span><span class="p">.</span><span class="nf">to_formatted_s</span><span class="p">(</span><span class="ss">:short</span><span class="p">)</span>
<span class="k">if</span> <span class="vi">@user</span><span class="p">.</span><span class="nf">current_sign_in_at</span>
<span class="vi">@last_login</span> <span class="o">=</span> <span class="vi">@user</span><span class="p">.</span><span class="nf">current_sign_in_at</span><span class="p">.</span><span class="nf">to_formatted_s</span><span class="p">(</span><span class="ss">:short</span><span class="p">)</span>
<span class="k">else</span>
<span class="vi">@last_login</span> <span class="o">=</span> <span class="s1">'never'</span>
<span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>
<p>Also, to show us which users are associated with a particular role, add the association to the <code class="language-plaintext highlighter-rouge">Role</code> model.</p>
<p><code class="language-plaintext highlighter-rouge">app/models/role.rb</code>:</p>
<div class="language-rb highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">has_many</span> <span class="ss">:users</span><span class="p">,</span> <span class="ss">dependent: :restrict_with_exception</span>
</code></pre></div></div>
<p>The <code class="language-plaintext highlighter-rouge">restrict_with_exception</code> option will cause an <code class="language-plaintext highlighter-rouge">ActiveRecord::DeleteRestrictionError</code> exception to be raised if you try to delete a <code class="language-plaintext highlighter-rouge">Role</code> record, but it has associated <code class="language-plaintext highlighter-rouge">User</code> records.</p>
<p>Next, add the following to <code class="language-plaintext highlighter-rouge">app/views/roles/show.html.erb</code>:</p>
<div class="language-erb highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt"><p></span>
<span class="nt"><strong></span>Assosciated users:<span class="nt"></strong></span>
<span class="cp"><%=</span> <span class="vi">@assosciated_users</span> <span class="cp">%></span>
<span class="nt"></p></span>
</code></pre></div></div>
<p>And the logic to the <code class="language-plaintext highlighter-rouge">RolesController</code>:</p>
<div class="language-rb highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">show</span>
<span class="k">if</span> <span class="vi">@role</span><span class="p">.</span><span class="nf">users</span><span class="p">.</span><span class="nf">empty?</span>
<span class="vi">@assosciated_user</span> <span class="o">=</span> <span class="s1">'None'</span>
<span class="k">else</span>
<span class="vi">@assosciated_users</span> <span class="o">=</span> <span class="vi">@role</span><span class="p">.</span><span class="nf">users</span><span class="p">.</span><span class="nf">map</span><span class="p">(</span><span class="o">&</span><span class="ss">:name</span><span class="p">).</span><span class="nf">join</span><span class="p">(</span><span class="s1">', '</span><span class="p">)</span>
<span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>
<p>And there we go. In not very much code we have implemented a robust authentication solution for our app, as well as building a simple interface to administer users.</p>
<p>Take a moment to restart the app, have a play with what we’ve got so far and assure yourself that it is working.</p>
<h2 id="authorization-with-cancancan">Authorization With cancancan</h2>
<p><a href="https://github.com/CanCanCommunity/cancancan" title="Continuation of CanCan, the authorization Gem for Ruby on Rails">Cancancan</a> is an authorization library for Ruby on Rails which restricts what resources a given user is allowed to access. It is the continuation of the now defunct cancan project started by Ryan Bates (of Railscast fame).</p>
<p>To install it, run:</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>bundle add cancancan
</code></pre></div></div>
<p>CanCanCan expects a <code class="language-plaintext highlighter-rouge">current_user</code> method to exist in the controller, which we have thanks to Devise.</p>
<p>It also expects user permissions to be defined in an <code class="language-plaintext highlighter-rouge">Ability</code> class, for which it includes a generator:</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>rails g cancan:ability
</code></pre></div></div>
<p>This will generate a file at <code class="language-plaintext highlighter-rouge">app/models/ability.rb</code>. Edit it like so:</p>
<div class="language-rb highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">Ability</span>
<span class="kp">include</span> <span class="no">CanCan</span><span class="o">::</span><span class="no">Ability</span>
<span class="k">def</span> <span class="nf">initialize</span><span class="p">(</span><span class="n">user</span><span class="p">)</span>
<span class="k">if</span> <span class="n">user</span><span class="p">.</span><span class="nf">admin?</span>
<span class="n">can</span> <span class="ss">:manage</span><span class="p">,</span> <span class="ss">:all</span>
<span class="k">else</span>
<span class="n">can</span> <span class="ss">:read</span><span class="p">,</span> <span class="ss">:all</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>
<p>As you can see, abilities are defined with the method <code class="language-plaintext highlighter-rouge">can</code>. This method takes two parameters: the first is the action that we want to perform and the second is the model class that the action applies to. In the above example we are permitting the admin to perform any CRUD action on any resource within our app and restricting all other users to just being able to read.</p>
<p>We don’t care about the permissions of those users who have not yet logged in (you remember they should just be directed to the sign in page), but if we did (for example, if they could view items), you would have to initialize the <code class="language-plaintext highlighter-rouge">user</code> variable to something sensible. Otherwise you would end up checking for nil values all over the place.</p>
<div class="language-rb highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">initialize</span><span class="p">(</span><span class="n">user</span><span class="p">)</span>
<span class="n">user</span> <span class="o">||=</span> <span class="no">User</span><span class="p">.</span><span class="nf">new</span> <span class="c1"># Guest user</span>
<span class="o">...</span>
<span class="k">end</span>
</code></pre></div></div>
<p>Next we need to define an <code class="language-plaintext highlighter-rouge">admin?</code> method in our <code class="language-plaintext highlighter-rouge">User</code> model.</p>
<p><code class="language-plaintext highlighter-rouge">app/models/user.rb</code>:</p>
<div class="language-rb highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">admin?</span>
<span class="n">role</span><span class="p">.</span><span class="nf">name</span> <span class="o">==</span> <span class="s1">'Admin'</span>
<span class="k">end</span>
</code></pre></div></div>
<p>And in the <code class="language-plaintext highlighter-rouge">UsersController</code> we need to specify which users are authorized to do what. You can do this on a per action basis using the <code class="language-plaintext highlighter-rouge">authorize!</code> method, which in turn performs the <code class="language-plaintext highlighter-rouge">can?</code> check and raises an exception if needed.</p>
<p>For example to check if a given user can edit an item:</p>
<div class="language-rb highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">edit</span>
<span class="n">authorize!</span> <span class="ss">:edit</span><span class="p">,</span> <span class="vi">@item</span>
<span class="k">end</span>
</code></pre></div></div>
<p>However, repeating this across every action in our app would soon become tiresome. Luckily there is an easier way to do this if you are using RESTful style controllers, namely with the method <code class="language-plaintext highlighter-rouge">load_and_authorize_resource</code>. As the name suggests, this method loads the appropriate resource and authorizes it in a before action.</p>
<p>Place it at the top of your <code class="language-plaintext highlighter-rouge">ItemsController</code>.</p>
<div class="language-rb highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">ItemsController</span> <span class="o"><</span> <span class="no">ApplicationController</span>
<span class="n">before_action</span> <span class="ss">:authenticate_user!</span>
<span class="n">load_and_authorize_resource</span>
<span class="o">...</span>
<span class="k">end</span>
</code></pre></div></div>
<p>As this method loads the necessary resource for us based on the action we are performing, we can also remove any lines of code that set the instance variable in each action.</p>
<p>In the case of our <code class="language-plaintext highlighter-rouge">ItemsController</code> that would mean removing the following lines:</p>
<div class="language-diff highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">class ItemsController < ApplicationController
</span><span class="gd">- before_action :set_item, only: [:show, :edit, :update, :destroy]
</span>
def new
<span class="gd">- @item = Item.new
</span> end
def create
<span class="gd">- @item = Item.new(item_params)
</span> ...
end
private
<span class="gd">- def set_item
- @item = Item.find(params[:id])
- end
</span>
end
</code></pre></div></div>
<p>If you now restart the server and access <a href="http://localhost:3000/items">http://localhost:3000/items</a> as a regular user or as a seller, you will be in read only mode. As an admin, you will still be able to create, edit and delete records.</p>
<p>Now we need to repeat the above steps for our other two resources:</p>
<p><code class="language-plaintext highlighter-rouge">app/controllers/users_controller.rb</code></p>
<div class="language-diff highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">class UsersController < ApplicationController
</span><span class="gd">- before_action :set_user, only: [:show, :edit, :update, :destroy]
</span> before_action :authenticate_user!
<span class="gi">+ load_and_authorize_resource
</span>
def new
<span class="gd">- @user = User.new
</span> end
def create
<span class="gd">- @user = User.new(user_params)
</span> ...
end
private
<span class="gd">- def set_user
- @user = User.find(params[:id])
- end
</span><span class="p">end
</span></code></pre></div></div>
<p><code class="language-plaintext highlighter-rouge">app/controllers/roles_controller.rb</code></p>
<div class="language-diff highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">class RolesController < ApplicationController
</span><span class="gd">- before_action :set_role, only: [:show, :edit, :update, :destroy]
</span> before_action :authenticate_user!
<span class="gi">+ load_and_authorize_resource
</span>
def new
<span class="gd">- @role = Role.new
</span> end
def create
<span class="gd">- @role = Role.new(role_params)
</span> ...
end
private
<span class="gd">- def set_role
- @role = Role.find(params[:id])
- end
</span><span class="p">end
</span></code></pre></div></div>
<p>This ensures that regular users and sellers are also in read-only mode when accessing Roles and Users.</p>
<p><img src="https://res.cloudinary.com/hibbard/image/upload/v1593077285/rails-devise-cancancan/access-denied.png" alt="CanCan::AccessDenied error when accessing Roles" title="You will see an AccessDenied error when trying to access a protected resource without the correct priviledges" /></p>
<h2 id="a-nicer-error-page">A Nicer Error Page</h2>
<p>Next, we can ensure that our users see a nicer error page than the one with the AccessDenied exception they currently see when attempting to access something they are not authorized to.</p>
<p>We can do this using a method called <a href="http://api.rubyonrails.org/classes/ActiveSupport/Rescuable/ClassMethods.html" title="ActiveSupport::Rescuable::ClassMethods">rescue_from</a> that we can place in our <code class="language-plaintext highlighter-rouge">ApplicationController</code>. We’ll pass it a block inside of which we’ll make the application show a flash error message and redirect to the home page.</p>
<p><code class="language-plaintext highlighter-rouge">app/controllers/application_controller.rb</code></p>
<div class="language-rb highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">rescue_from</span> <span class="no">CanCan</span><span class="o">::</span><span class="no">AccessDenied</span> <span class="k">do</span>
<span class="n">flash</span><span class="p">[</span><span class="ss">:error</span><span class="p">]</span> <span class="o">=</span> <span class="s1">'Access denied!'</span>
<span class="n">redirect_to</span> <span class="n">root_url</span>
<span class="k">end</span>
</code></pre></div></div>
<p>Now all that remains to do is to set the permissions of regular users and sellers to something sensible.</p>
<p>Let’s define two more methods to check the user’s current role:</p>
<p><code class="language-plaintext highlighter-rouge">app/models/user.rb</code></p>
<div class="language-rb highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">seller?</span>
<span class="n">role</span><span class="p">.</span><span class="nf">name</span> <span class="o">==</span> <span class="s1">'Seller'</span>
<span class="k">end</span>
<span class="k">def</span> <span class="nf">regular?</span>
<span class="n">role</span><span class="p">.</span><span class="nf">name</span> <span class="o">==</span> <span class="s1">'Regular'</span>
<span class="k">end</span>
</code></pre></div></div>
<p>Then it’s a matter of updating the abilities in the Ability class.</p>
<p><code class="language-plaintext highlighter-rouge">app/models/ability.rb</code></p>
<div class="language-rb highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">if</span> <span class="n">user</span><span class="p">.</span><span class="nf">admin?</span>
<span class="n">can</span> <span class="ss">:manage</span><span class="p">,</span> <span class="ss">:all</span>
<span class="k">elsif</span> <span class="n">user</span><span class="p">.</span><span class="nf">seller?</span>
<span class="n">can</span> <span class="ss">:read</span><span class="p">,</span> <span class="no">Item</span>
<span class="n">can</span> <span class="ss">:create</span><span class="p">,</span> <span class="no">Item</span>
<span class="n">can</span> <span class="ss">:update</span><span class="p">,</span> <span class="no">Item</span> <span class="k">do</span> <span class="o">|</span><span class="n">item</span><span class="o">|</span>
<span class="n">item</span><span class="p">.</span><span class="nf">try</span><span class="p">(</span><span class="ss">:user</span><span class="p">)</span> <span class="o">==</span> <span class="n">user</span>
<span class="k">end</span>
<span class="n">can</span> <span class="ss">:destroy</span><span class="p">,</span> <span class="no">Item</span> <span class="k">do</span> <span class="o">|</span><span class="n">item</span><span class="o">|</span>
<span class="n">item</span><span class="p">.</span><span class="nf">try</span><span class="p">(</span><span class="ss">:user</span><span class="p">)</span> <span class="o">==</span> <span class="n">user</span>
<span class="k">end</span>
<span class="k">elsif</span> <span class="n">user</span><span class="p">.</span><span class="nf">regular?</span>
<span class="n">can</span> <span class="ss">:read</span><span class="p">,</span> <span class="no">Item</span>
<span class="k">end</span>
</code></pre></div></div>
<p>The permissions for admins and regular users should be straight forward.</p>
<p>In the case of sellers however, things become a little more complicated when dealing with the update and destroy actions (you remember that sellers should only be able to update and delete their own items).</p>
<p>Here we pass <code class="language-plaintext highlighter-rouge">can</code> a block which will receive the instance of the model we’re checking. The block should return <code class="language-plaintext highlighter-rouge">true</code> or <code class="language-plaintext highlighter-rouge">false</code> depending on whether the action should be allowed, or not. Within the block we’ll use Rails’ <a href="http://apidock.com/rails/Object/try" title="Invokes the public method whose name goes as first argument. If the receiver does not respond to it the call returns nil rather than raising an exception">try</a> method to check that the item’s <code class="language-plaintext highlighter-rouge">user</code> attribute is the current user. Using <code class="language-plaintext highlighter-rouge">try</code> covers the eventuality that the item is <code class="language-plaintext highlighter-rouge">nil</code> and will prevent an exception being raised.</p>
<p>Finally, so that regular users and sellers don’t see any links to actions they are not permitted to perform, alter <code class="language-plaintext highlighter-rouge">app/views/items/index.html.erb</code> like so:</p>
<div class="language-erb highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt"><td></span>
<span class="cp"><%</span> <span class="k">if</span> <span class="n">can?</span> <span class="ss">:update</span><span class="p">,</span> <span class="n">item</span> <span class="cp">%></span>
<span class="cp"><%=</span> <span class="n">link_to</span> <span class="s1">'Edit'</span><span class="p">,</span> <span class="n">edit_item_path</span><span class="p">(</span><span class="n">item</span><span class="p">)</span> <span class="cp">%></span>
<span class="cp"><%</span> <span class="k">end</span> <span class="cp">%></span>
<span class="nt"></td></span>
<span class="nt"><td></span>
<span class="cp"><%</span> <span class="k">if</span> <span class="n">can?</span> <span class="ss">:destroy</span><span class="p">,</span> <span class="n">item</span> <span class="cp">%></span>
<span class="cp"><%=</span> <span class="n">link_to</span> <span class="s1">'Destroy'</span><span class="p">,</span> <span class="n">item</span><span class="p">,</span> <span class="ss">method: :delete</span><span class="p">,</span> <span class="ss">data: </span><span class="p">{</span> <span class="ss">confirm: </span><span class="s1">'Are you sure?'</span> <span class="p">}</span> <span class="cp">%></span>
<span class="cp"><%</span> <span class="k">end</span> <span class="cp">%></span>
<span class="nt"></td></span>
...
<span class="cp"><%</span> <span class="k">if</span> <span class="n">can?</span> <span class="ss">:create</span><span class="p">,</span> <span class="no">Item</span> <span class="cp">%></span>
<span class="cp"><%=</span> <span class="n">link_to</span> <span class="s1">'New Item'</span><span class="p">,</span> <span class="n">new_item_path</span> <span class="cp">%></span>
<span class="cp"><%</span> <span class="k">end</span> <span class="cp">%></span>
</code></pre></div></div>
<h2 id="the-finishing-touches">The Finishing Touches</h2>
<p>Before we end, let’s add some navigation to <code class="language-plaintext highlighter-rouge">app/views/layouts/application.html.erb</code> so as to help admin users switch between resources.</p>
<div class="language-erb highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt"><body></span>
<span class="nt"><div</span> <span class="na">class=</span><span class="s">"flex-container"</span><span class="nt">></span>
<span class="nt"><header></span>
<span class="cp"><%</span> <span class="k">if</span> <span class="n">user_signed_in?</span> <span class="cp">%></span>
Signed in as <span class="cp"><%=</span> <span class="n">current_user</span><span class="p">.</span><span class="nf">email</span> <span class="cp">%></span>.<span class="nt"><br></span>
Not you?
<span class="cp"><%=</span> <span class="n">link_to</span> <span class="s2">"Edit profile"</span><span class="p">,</span> <span class="n">edit_user_registration_path</span> <span class="cp">%></span>
<span class="cp"><%=</span> <span class="n">link_to</span> <span class="s2">"Sign out"</span><span class="p">,</span> <span class="n">destroy_user_session_path</span><span class="p">,</span> <span class="ss">method: :delete</span> <span class="cp">%></span>
<span class="cp"><%</span> <span class="k">else</span> <span class="cp">%></span>
<span class="cp"><%=</span> <span class="n">link_to</span> <span class="s2">"Sign up"</span><span class="p">,</span> <span class="n">new_user_registration_path</span> <span class="cp">%></span> or
<span class="cp"><%=</span> <span class="n">link_to</span> <span class="s2">"sign in"</span><span class="p">,</span> <span class="n">new_user_session_path</span> <span class="cp">%></span>
<span class="cp"><%</span> <span class="k">end</span> <span class="cp">%></span>
<span class="nt"><nav></span>
<span class="cp"><%</span> <span class="k">if</span> <span class="vi">@current_user</span><span class="o">&</span><span class="p">.</span><span class="nf">admin?</span> <span class="cp">%></span>
<span class="cp"><%=</span> <span class="n">link_to</span> <span class="s2">"Items"</span><span class="p">,</span> <span class="n">items_path</span> <span class="cp">%></span> |
<span class="cp"><%=</span> <span class="n">link_to</span> <span class="s2">"Users"</span><span class="p">,</span> <span class="n">users_path</span> <span class="cp">%></span> |
<span class="cp"><%=</span> <span class="n">link_to</span> <span class="s2">"Roles"</span><span class="p">,</span> <span class="n">roles_path</span> <span class="cp">%></span>
<span class="cp"><%</span> <span class="k">end</span> <span class="cp">%></span>
<span class="nt"></nav></span>
<span class="nt"></header></span>
<span class="cp"><%</span> <span class="n">flash</span><span class="p">.</span><span class="nf">each</span> <span class="k">do</span> <span class="o">|</span><span class="nb">name</span><span class="p">,</span> <span class="n">msg</span><span class="o">|</span> <span class="cp">%></span>
<span class="cp"><%=</span> <span class="n">content_tag</span> <span class="ss">:div</span><span class="p">,</span> <span class="n">msg</span><span class="p">,</span> <span class="ss">id: </span><span class="s2">"flash_</span><span class="si">#{</span><span class="nb">name</span><span class="si">}</span><span class="s2">"</span> <span class="cp">%></span>
<span class="cp"><%</span> <span class="k">end</span> <span class="cp">%></span>
<span class="nt"><main></span>
<span class="cp"><%=</span> <span class="k">yield</span> <span class="cp">%></span>
<span class="nt"></main></span>
<span class="nt"></div></span>
<span class="nt"></body></span>
</code></pre></div></div>
<p>The extra markup lets us add some basic styling to make the app more visually appealing. I’m not going to list the CSS here, you can copy it out of the <a href="https://github.com/jameshibbard/authentication-with-devise-and-cancancan/blob/master/app/assets/stylesheets/scaffolds.scss">scaffold.scss file on GitHub</a>. The table styling is courtesy of <a href="https://www.w3schools.com/Css/css_table.asp">W3schools</a>.</p>
<p>When you’ve applied the styles, here’s what the app should look like:</p>
<p><img src="https://res.cloudinary.com/hibbard/image/upload/v1593077992/rails-devise-cancancan/finished-app.png" alt="The finished app listing items in the store" title="The finished app listing items in the store" /></p>
<p>Next, we can add some JavaScript to fade out, then remove our flash messages after a delay of 3.5 seconds. To this end, make a new folder named <code class="language-plaintext highlighter-rouge">src</code> in the <code class="language-plaintext highlighter-rouge">app/javascript</code> folder. In <code class="language-plaintext highlighter-rouge">app/javascript/src</code> create a file named <code class="language-plaintext highlighter-rouge">index.js</code> and add the following:</p>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">document</span><span class="p">.</span><span class="nx">addEventListener</span><span class="p">(</span><span class="dl">'</span><span class="s1">DOMContentLoaded</span><span class="dl">'</span><span class="p">,</span> <span class="p">()</span> <span class="o">=></span> <span class="p">{</span>
<span class="kd">const</span> <span class="nx">flashMessage</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="dl">'</span><span class="s1">div[id^="flash_"]</span><span class="dl">'</span><span class="p">);</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">flashMessage</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">setTimeout</span><span class="p">(()</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">flashMessage</span><span class="p">.</span><span class="nx">classList</span><span class="p">.</span><span class="nx">add</span><span class="p">(</span><span class="dl">'</span><span class="s1">hide</span><span class="dl">'</span><span class="p">);</span>
<span class="nx">flashMessage</span><span class="p">.</span><span class="nx">classList</span><span class="p">.</span><span class="nx">remove</span><span class="p">(</span><span class="dl">'</span><span class="s1">show</span><span class="dl">'</span><span class="p">);</span>
<span class="nx">setTimeout</span><span class="p">(()</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">flashMessage</span><span class="p">.</span><span class="nx">parentElement</span><span class="p">.</span><span class="nx">removeChild</span><span class="p">(</span><span class="nx">flashMessage</span><span class="p">);</span>
<span class="p">},</span> <span class="mi">1500</span><span class="p">);</span>
<span class="p">},</span> <span class="mi">3500</span><span class="p">);</span>
<span class="p">}</span>
<span class="p">});</span>
</code></pre></div></div>
<p>Then require it in <code class="language-plaintext highlighter-rouge">app/javascript/packs/application.js</code> to ensure it is included with our JavaScript bundle.</p>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">require</span><span class="p">(</span><span class="dl">'</span><span class="s1">@rails/ujs</span><span class="dl">'</span><span class="p">).</span><span class="nx">start</span><span class="p">();</span>
<span class="nx">require</span><span class="p">(</span><span class="dl">'</span><span class="s1">turbolinks</span><span class="dl">'</span><span class="p">).</span><span class="nx">start</span><span class="p">();</span>
<span class="nx">require</span><span class="p">(</span><span class="dl">'</span><span class="s1">@rails/activestorage</span><span class="dl">'</span><span class="p">).</span><span class="nx">start</span><span class="p">();</span>
<span class="nx">require</span><span class="p">(</span><span class="dl">'</span><span class="s1">channels</span><span class="dl">'</span><span class="p">);</span>
<span class="nx">require</span><span class="p">(</span><span class="dl">'</span><span class="s1">../src/index</span><span class="dl">'</span><span class="p">);</span>
</code></pre></div></div>
<p>Finally, the very last thing I want to examine (promise!) is a slightly lesser documented feature of Devise. You remember that we initially wanted users who are not logged in to be redirected to the sign-in page? Well, wouldn’t it be nicer if we directed them to a welcome page with a link to either register or sign in?</p>
<p>To do this we need a <code class="language-plaintext highlighter-rouge">WelcomeController</code> and a corresponding view. These can be generated with the following command:</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>rails g controller welcome index
</code></pre></div></div>
<p>Now edit the newly generated view template at <code class="language-plaintext highlighter-rouge">app/views/welcome/index.html.erb</code>:</p>
<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt"><h1></span>Welcome to the Store!<span class="nt"></h1></span>
<span class="nt"><h2></span>Selling you things you don't need since 2020<span class="nt"></h2></span>
<span class="nt"><p></span>
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Adipisci
eveniet, repellat quae sed hic obcaecati nam exercitationem saepe
quod totam dolore explicabo culpa iure deserunt? Dignissimos, fuga,
adipisci. Temporibus, aliquid.
<span class="nt"></p></span>
</code></pre></div></div>
<p>Once that is in place, alter your <code class="language-plaintext highlighter-rouge">config/routes.rb</code> to take advantage of Devise’s <code class="language-plaintext highlighter-rouge">authenticated</code> route thus:</p>
<div class="language-rb highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">authenticated</span> <span class="ss">:user</span> <span class="k">do</span>
<span class="n">root</span> <span class="ss">to: </span><span class="s1">'items#index'</span><span class="p">,</span> <span class="ss">as: :authenticated_root</span>
<span class="k">end</span>
<span class="n">root</span> <span class="ss">to: </span><span class="s1">'welcome#index'</span>
</code></pre></div></div>
<p>Restart your server and we’re done!</p>
<p>In case you missed it at the beginning, the code for this tutorial is on GitHub: <a href="https://github.com/hibbard-eu/authentication-with-devise-and-cancancan" title="Project code">https://github.com/hibbard-eu/authentication-with-devise-and-cancancan</a></p>
<p>This was quite a long post — I hope it proves useful for people. If you have any questions or comments, I’d be glad to hear them below.</p>James HibbardThis is a tutorial on how to set up authentication (verifying who you are) and authorization (what you are permitted to do) using Ruby 2.7, Rails 6.0.3 and two popular Ruby gems: Devise and cancancan. The code for this tutorial is on GitHub: https://github.com/hibbard-eu/authentication-with-devise-and-cancancanMake a Filterable Table With Vue.js2020-03-20T00:00:00+00:002020-03-20T00:00:00+00:00https://hibbard.eu/make-a-filterable-table-with-vue<p>One of the things I love about Vue is its unobtrusive reactivity system. Models are plain JavaScript objects and when you modify them, Vue automatically updates a page’s HTML to reflect the change. This makes state management easy and intuitive.</p>
<p>In this tutorial, I’ll demonstrate how to leverage Vue’s reactivity to build a filterable table. This will only display the rows that match whatever text a user has entered into a text input. I’ll also show you how to highlight the matches.</p>
<p>This might be useful to help users quickly find what they are looking for in a long table. Once you have understood how it works, you can easily adapt it to lists or anything else you need to filter.</p>
<!--more-->
<p>For the impatient, there is a demo of what we’ll end up with <a href="#the-finished-thing-on-codepen">at the end of the article</a> and also <a href="https://codepen.io/James_Hibbard/pen/wvaEaom">on CodePen</a>.</p>
<h2 id="basic-setup">Basic Setup</h2>
<p>This is the skeleton HTML we will be using.</p>
<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp"><!DOCTYPE html></span>
<span class="nt"><html</span> <span class="na">lang=</span><span class="s">"en"</span><span class="nt">></span>
<span class="nt"><head></span>
<span class="nt"><meta</span> <span class="na">charset=</span><span class="s">"UTF-8"</span> <span class="nt">/></span>
<span class="nt"><title></span>Filterable table<span class="nt"></title></span>
<span class="nt"></head></span>
<span class="nt"><body></span>
<span class="nt"><div</span> <span class="na">id=</span><span class="s">"app"</span><span class="nt">></div></span>
<span class="nt"><script </span><span class="na">src=</span><span class="s">"https://cdn.jsdelivr.net/npm/vue@2.6.11/dist/vue.js"</span><span class="nt">></script></span>
<span class="nt"><script></span>
<span class="kd">const</span> <span class="nx">app</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">Vue</span><span class="p">({</span>
<span class="na">el</span><span class="p">:</span> <span class="dl">'</span><span class="s1">#app</span><span class="dl">'</span><span class="p">,</span>
<span class="na">data</span><span class="p">:</span> <span class="p">{},</span>
<span class="na">methods</span><span class="p">:</span> <span class="p">{},</span>
<span class="na">computed</span><span class="p">:</span> <span class="p">{},</span>
<span class="p">});</span>
<span class="nt"></script></span>
<span class="nt"></body></span>
<span class="nt"></html></span>
</code></pre></div></div>
<p>As you can see we are pulling in Vue from a CDN, then rendering an empty Vue app in the div element with the ID of <code class="language-plaintext highlighter-rouge">app</code>.</p>
<p>Now let’s get a table displaying. We’ll add the data for the rows as a <code class="language-plaintext highlighter-rouge">data</code> property on the Vue instance and we’ll use a <a href="https://vuejs.org/v2/api/#v-for">v-for directive</a> to display each row.</p>
<p>Here’s the HTML:</p>
<div class="language-vue highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt"><div</span> <span class="na">id=</span><span class="s">"app"</span><span class="nt">></span>
<span class="nt"><table></span>
<span class="nt"><thead></span>
<span class="nt"><tr></span>
<span class="nt"><th></span>Department<span class="nt"></th></span>
<span class="nt"><th></span>Employees<span class="nt"></th></span>
<span class="nt"></tr></span>
<span class="nt"></thead></span>
<span class="nt"><tbody></span>
<span class="nt"><tr</span> <span class="na">v-for=</span><span class="s">"(row, index) in rows"</span> <span class="na">:key=</span><span class="s">"`employee-${index}`"</span><span class="nt">></span>
<span class="nt"><td></span>{{ row.department }}<span class="nt"></td></span>
<span class="nt"><td></span>{{ [...row.employees].sort().join(', ') }}<span class="nt"></td></span>
<span class="nt"></tr></span>
<span class="nt"></tbody></span>
<span class="nt"></table></span>
<span class="nt"></div></span>
</code></pre></div></div>
<p>And here’s the JavaScript:</p>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">app</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">Vue</span><span class="p">({</span>
<span class="na">el</span><span class="p">:</span> <span class="dl">'</span><span class="s1">#app</span><span class="dl">'</span><span class="p">,</span>
<span class="na">data</span><span class="p">:</span> <span class="p">{</span>
<span class="na">rows</span><span class="p">:</span> <span class="p">[</span>
<span class="p">{</span> <span class="na">department</span><span class="p">:</span> <span class="dl">'</span><span class="s1">Accounting</span><span class="dl">'</span><span class="p">,</span> <span class="na">employees</span><span class="p">:</span> <span class="p">[</span><span class="dl">'</span><span class="s1">Bradley</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">Jones</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">Alvarado</span><span class="dl">'</span><span class="p">]</span> <span class="p">},</span>
<span class="p">{</span> <span class="na">department</span><span class="p">:</span> <span class="dl">'</span><span class="s1">Human Resources</span><span class="dl">'</span><span class="p">,</span> <span class="na">employees</span><span class="p">:</span> <span class="p">[</span><span class="dl">'</span><span class="s1">Juarez</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">Banks</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">Smith</span><span class="dl">'</span><span class="p">]</span> <span class="p">},</span>
<span class="p">{</span> <span class="na">department</span><span class="p">:</span> <span class="dl">'</span><span class="s1">Production</span><span class="dl">'</span><span class="p">,</span> <span class="na">employees</span><span class="p">:</span> <span class="p">[</span><span class="dl">'</span><span class="s1">Sweeney</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">Bartlett</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">Singh</span><span class="dl">'</span><span class="p">]</span> <span class="p">},</span>
<span class="p">{</span> <span class="na">department</span><span class="p">:</span> <span class="dl">'</span><span class="s1">Research and Development</span><span class="dl">'</span><span class="p">,</span> <span class="na">employees</span><span class="p">:</span> <span class="p">[</span><span class="dl">'</span><span class="s1">Lambert</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">Williamson</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">Smith</span><span class="dl">'</span><span class="p">]</span> <span class="p">},</span>
<span class="p">{</span> <span class="na">department</span><span class="p">:</span> <span class="dl">'</span><span class="s1">Sales and Marketing</span><span class="dl">'</span><span class="p">,</span> <span class="na">employees</span><span class="p">:</span> <span class="p">[</span><span class="dl">'</span><span class="s1">Prince</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">Townsend</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">Jones</span><span class="dl">'</span><span class="p">]</span> <span class="p">}</span>
<span class="p">]</span>
<span class="p">},</span>
<span class="na">methods</span><span class="p">:</span> <span class="p">{},</span>
<span class="na">computed</span><span class="p">:</span> <span class="p">{},</span>
<span class="p">});</span>
</code></pre></div></div>
<p>There are a couple of things to notice here.</p>
<p>We have declared an <code class="language-plaintext highlighter-rouge">index</code> variable inside the <code class="language-plaintext highlighter-rouge">v-for</code> directive, which we are using in our <a href="https://vuejs.org/v2/api/#key">key attribute</a>. This is used by Vue’s virtual DOM algorithm to improve performance.</p>
<p>As the employees aren’t listed in alphabetical order in the data property, we are using <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort">.sort()</a> to sort them as they are rendered to the page. As <code class="language-plaintext highlighter-rouge">.sort()</code> mutates the array, we are using the spread syntax to create a copy of the array first. If we didn’t do this, Vue would complain about an infinite loop in a render function.</p>
<p>Finally, once we have sorted the array and joined the entries, we are converting it to a string before rendering it to the page.</p>
<p>At this point you should have a basic table displaying.</p>
<h2 id="now-lets-add-some-filtering">Now Let’s Add Some Filtering</h2>
<p>First off, let’s add a text input for the user to type into. We’ll use a <a href="https://vuejs.org/v2/api/#v-model">v-model directive</a> to create a two-way binding with a data property.</p>
<div class="language-vue highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt"><div</span> <span class="na">id=</span><span class="s">"app"</span><span class="nt">></span>
<span class="nt"><table></span>
...
<span class="nt"></table></span>
<span class="nt"><input</span> <span class="na">type=</span><span class="s">"text"</span>
<span class="na">placeholder=</span><span class="s">"Filter by department or employee"</span>
<span class="na">v-model=</span><span class="s">"filter"</span> <span class="nt">/></span>
<span class="nt"></div></span>
</code></pre></div></div>
<p>Next, add the <code class="language-plaintext highlighter-rouge">filter</code> data property to the Vue instance:</p>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">data</span><span class="p">:</span> <span class="p">{</span>
<span class="nl">filter</span><span class="p">:</span><span class="dl">''</span><span class="p">,</span>
<span class="nx">rows</span><span class="p">:</span> <span class="p">[</span> <span class="p">...</span> <span class="p">],</span>
<span class="p">},</span>
</code></pre></div></div>
<p>Finally, in order to filter the table for specific employees, we’ll make use of Vue’s <a href="https://vuejs.org/v2/guide/computed.html#Computed-Properties">computed properties</a>. These help you avoid putting too much logic in your templates. They are also cached (as long as none of their reactive dependencies changes), meaning better performance.</p>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">computed</span><span class="p">:</span> <span class="p">{</span>
<span class="nx">filteredRows</span><span class="p">()</span> <span class="p">{</span>
<span class="k">return</span> <span class="k">this</span><span class="p">.</span><span class="nx">rows</span><span class="p">.</span><span class="nx">filter</span><span class="p">(</span><span class="nx">row</span> <span class="o">=></span> <span class="p">{</span>
<span class="kd">const</span> <span class="nx">employees</span> <span class="o">=</span> <span class="nx">row</span><span class="p">.</span><span class="nx">employees</span><span class="p">.</span><span class="nx">toString</span><span class="p">().</span><span class="nx">toLowerCase</span><span class="p">();</span>
<span class="kd">const</span> <span class="nx">department</span> <span class="o">=</span> <span class="nx">row</span><span class="p">.</span><span class="nx">department</span><span class="p">.</span><span class="nx">toLowerCase</span><span class="p">();</span>
<span class="kd">const</span> <span class="nx">searchTerm</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">filter</span><span class="p">.</span><span class="nx">toLowerCase</span><span class="p">();</span>
<span class="k">return</span> <span class="nx">department</span><span class="p">.</span><span class="nx">includes</span><span class="p">(</span><span class="nx">searchTerm</span><span class="p">)</span> <span class="o">||</span>
<span class="nx">employees</span><span class="p">.</span><span class="nx">includes</span><span class="p">(</span><span class="nx">searchTerm</span><span class="p">);</span>
<span class="p">});</span>
<span class="p">}</span>
<span class="p">},</span>
</code></pre></div></div>
<p>Here, we’re using JavaScript’s native <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/filter">filter method</a> to return a new array containing any elements that match the search term. We’re lowercasing everything to ensure that the search is case insensitive.</p>
<p>We’ll also need to update the HTML to make use of our computed property:</p>
<div class="language-vue highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt"><tr</span> <span class="na">v-for=</span><span class="s">"(row, index) in filteredRows"</span> <span class="na">:key=</span><span class="s">"`employee-${index}`"</span><span class="nt">></span>
...
<span class="nt"></tr></span>
</code></pre></div></div>
<p>If you run the code at this point, you should have a table which you can filter by department and by employee.</p>
<h2 id="how-to-highlight-the-matches">How to Highlight the Matches</h2>
<p>As a final touch, let’s highlight the text in the table rows that matches what the user has entered into the text input.</p>
<p>We can do this using a method which we’ll call <code class="language-plaintext highlighter-rouge">highlightMatches</code>.</p>
<div class="language-vue highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt"><tr</span> <span class="na">v-for=</span><span class="s">"(row, index) in filteredRows"</span> <span class="na">:key=</span><span class="s">"`employee-${index}`"</span><span class="nt">></span>
<span class="nt"><td</span> <span class="na">v-html=</span><span class="s">"highlightMatches(row.department)"</span><span class="nt">></td></span>
<span class="nt"><td</span> <span class="na">v-html=</span><span class="s">"highlightMatches([...row.employees].sort().join(', '))"</span><span class="nt">></td></span>
<span class="nt"></tr></span>
</code></pre></div></div>
<p>Our method will return whatever text it is passed, with <code class="language-plaintext highlighter-rouge"><strong></code> tags wrapped around any matches. So that Vue interprets the <code class="language-plaintext highlighter-rouge"><strong></code> tags as HTML and doesn’t simply output them to the page, we’re using the <a href="https://vuejs.org/v2/api/#v-html">v-html directive</a> in our template.</p>
<p>This is what the method looks like:</p>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">methods</span><span class="p">:</span> <span class="p">{</span>
<span class="nx">highlightMatches</span><span class="p">(</span><span class="nx">text</span><span class="p">)</span> <span class="p">{</span>
<span class="kd">const</span> <span class="nx">matchExists</span> <span class="o">=</span> <span class="nx">text</span><span class="p">.</span><span class="nx">toLowerCase</span><span class="p">().</span><span class="nx">includes</span><span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="nx">filter</span><span class="p">.</span><span class="nx">toLowerCase</span><span class="p">());</span>
<span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">matchExists</span><span class="p">)</span> <span class="k">return</span> <span class="nx">text</span><span class="p">;</span>
<span class="kd">const</span> <span class="nx">re</span> <span class="o">=</span> <span class="k">new</span> <span class="nb">RegExp</span><span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="nx">filter</span><span class="p">,</span> <span class="dl">'</span><span class="s1">ig</span><span class="dl">'</span><span class="p">);</span>
<span class="k">return</span> <span class="nx">text</span><span class="p">.</span><span class="nx">replace</span><span class="p">(</span><span class="nx">re</span><span class="p">,</span> <span class="nx">matchedText</span> <span class="o">=></span> <span class="s2">`<strong></span><span class="p">${</span><span class="nx">matchedText</span><span class="p">}</span><span class="s2"></strong>`</span><span class="p">);</span>
<span class="p">}</span>
<span class="p">},</span>
</code></pre></div></div>
<p>First we are looking to see if the text the method is passed contains whatever the user has typed in. If there is no match, we are simply returning the text as is. This will deal with cases when, for example, the match is present in the employees column, but there is nothing to highlight in the department column. Again we are using <code class="language-plaintext highlighter-rouge">.toLowerCase()</code> to make things case insensitive.</p>
<p>Assuming a match exists, we are creating a rexeg with whatever the user has typed in. We are then returning the text with the matched part wrapped in <code class="language-plaintext highlighter-rouge"><strong></code> tags.</p>
<h2 id="the-finished-thing-on-codepen">The Finished Thing on CodePen</h2>
<p>And that’s all there is to it. Here is the final result running on CodePen with a little styling applied.</p>
<p class="codepen" data-height="400" data-theme-id="dark" data-default-tab="result" data-user="James_Hibbard" data-slug-hash="wvaEaom" style="height: 400px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;" data-pen-title="Filterable Table With Vue.js ">
<span>See the Pen <a href="https://codepen.io/James_Hibbard/pen/wvaEaom">
Filterable Table With Vue.js </a> by James Hibbard (<a href="https://codepen.io/James_Hibbard">@James_Hibbard</a>)
on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async="" src="https://static.codepen.io/assets/embed/ei.js"></script>
<h2 id="why-might-this-not-be-a-good-idea">Why Might This Not Be a Good Idea?</h2>
<p>Before we end, it’s worth mentioning that this approach has a couple of downsides.</p>
<ul>
<li>As we are using JavaScript to render the table to the page, this will give us an SEO hit. Although search bots can supposedly parse and execute JavaScript, I wouldn’t like to bet on what they see when they find our table. If SEO is important to you, you will need to look into <a href="https://vuejs.org/v2/guide/ssr.html">server-side rendering</a> with something like Nuxt.</li>
<li>We are offering people with JavaScript turned off a bad experience. In an ideal world, we would offer them some kind of fallback. If you think that nowadays everyone has JavaScript enabled, look <a href="https://kryogenix.org/code/browser/everyonehasjs.html">here</a>.</li>
</ul>
<h2 id="conclusion">Conclusion</h2>
<p>In this post I have demonstrated how to leverage Vue’s reactivity system to build a filterable table in only a few lines of code.</p>
<p>If you have any questions or comments, I’d be glad hear them below.</p>James HibbardOne of the things I love about Vue is its unobtrusive reactivity system. Models are plain JavaScript objects and when you modify them, Vue automatically updates a page’s HTML to reflect the change. This makes state management easy and intuitive. In this tutorial, I’ll demonstrate how to leverage Vue’s reactivity to build a filterable table. This will only display the rows that match whatever text a user has entered into a text input. I’ll also show you how to highlight the matches. This might be useful to help users quickly find what they are looking for in a long table. Once you have understood how it works, you can easily adapt it to lists or anything else you need to filter.How to Install Ubuntu Server on VirtualBox2019-12-11T00:00:00+00:002019-12-11T00:00:00+00:00https://hibbard.eu/how-to-install-ubuntu-server-virtual-box<p>In this post I’ll show you how to install Ubuntu Server 22.04 LTS (Jammy Jellyfish) on Oracle’s VirtualBox. I’ll also demonstrate how to connect to the Ubuntu instance via SSH, as well as how to run VirtualBox in headless mode.</p>
<p>Let’s get started!</p>
<!--more-->
<hr />
<p><strong>Note:</strong> this post was updated on 28<sup>th</sup> May, 2022 to use the latest versions of Ubuntu Server and VirtualBox.</p>
<hr />
<h2 id="what-is-virtualbox">What Is VirtualBox?</h2>
<p>VirtualBox is a software virtualization package that you can install on your operating system (just as you would a normal program). It supports the creation and management of virtual machines into which you can install a second operating system.</p>
<p>In VirtualBox terminology, the operating system on which you install VirtualBox (i.e. your regular OS) is called the <em>host</em>. The operating system you install within VirtualBox (i.e. inside the virtual machine) is called the <em>guest</em>.</p>
<p>For this tutorial, I’ll be using Linux Mint 20.3 as the host OS, but there’s no reason you couldn’t use a different Linux distro, or macOS, or Windows (if you’re so inclined).</p>
<h2 id="install-virtualbox">Install VirtualBox</h2>
<p>The first thing to do is to get VirtualBox installed. I’ll not go into much detail here, as there are comprehensive instructions for all of the main operating systems <a href="https://www.virtualbox.org/wiki/Downloads">on the project’s homepage</a>.</p>
<p>Personally, I downloaded and installed the deb package for Ubuntu 20.04. This is because the VirtualBox version in the Mint repos is slightly outdated and I wanted to be running the latest version.</p>
<h2 id="download-ubuntu-server">Download Ubuntu Server</h2>
<p>The next thing to do is to grab a copy of Ubuntu Server. You can do this from their <a href="https://www.ubuntu.com/download/server">download page</a>. Select option 2 (<em>Manual server installation</em>) which will download a 1.37GB ISO file to your PC.</p>
<p>At the time of writing the current LTS version is Ubuntu Server 22.04 and this is what I’ll be using. It’s supported until April 2025 and is available 64-bit.</p>
<h2 id="create-a-new-virtual-machine">Create a New Virtual Machine</h2>
<p>Start up VirtualBox. This should open the VirtualBox Manager, the interface from which you will administer all of your virtual machines.</p>
<p><img src="https://res.cloudinary.com/hibbard/image/upload/v1576058633/ubuntu-22.04-virtualbox-6.1.32/virtualbox-manager-1.png" alt="Welcome to VirtualBox" /></p>
<p>Next Click on <em>New</em> (in the top right of the VirtualBox Manager), give your virtual machine a name and the two drop down menus should automatically update.</p>
<p><img src="https://res.cloudinary.com/hibbard/image/upload/v1576058633/ubuntu-22.04-virtualbox-6.1.32/virtualbox-manager-2.png" alt="Name and operating system" /></p>
<p>Click <em>Next</em>. The wizard will now ask you to select the amount of memory (RAM) in megabytes to be allocated to the virtual machine. I chose 2GB (2048 megabytes).</p>
<p><img src="https://res.cloudinary.com/hibbard/image/upload/v1576058633/ubuntu-22.04-virtualbox-6.1.32/virtualbox-manager-3.png" alt="Memory size" /></p>
<p>Click <em>Next</em> and you will be prompted to add a virtual hard disk to the new machine. Make sure that <em>Create a virtual hard disk now</em> is selected, then press <em>Create</em>.</p>
<p><img src="https://res.cloudinary.com/hibbard/image/upload/v1576058633/ubuntu-22.04-virtualbox-6.1.32/virtualbox-manager-4.png" alt="Hard disk" /></p>
<p>Now we need to choose the file type for the new virtual hard disk. Make sure that <em>VDI (VirtualBox Disk Image)</em> is checked and press <em>Next</em>.</p>
<p><img src="https://res.cloudinary.com/hibbard/image/upload/v1576058634/ubuntu-22.04-virtualbox-6.1.32/virtualbox-manager-5.png" alt="Hard disk file type" /></p>
<p>On the next screen you will be asked whether the new virtual hard disk should grow as it is used (dynamically allocated) or if it should be created at its maximum size. Make sure that <em>dynamically allocated</em> is selected, then click <em>Next</em>.</p>
<p><img src="https://res.cloudinary.com/hibbard/image/upload/v1576058634/ubuntu-22.04-virtualbox-6.1.32/virtualbox-manager-6.png" alt="Storage on physical hard disk" /></p>
<p>Finally, select the size of the virtual hard disk in megabytes. The default size of 10GB should be plenty, but feel free to increase this as you see fit. Then click <em>Create</em>.</p>
<p><img src="https://res.cloudinary.com/hibbard/image/upload/v1576058634/ubuntu-22.04-virtualbox-6.1.32/virtualbox-manager-7.png" alt="File location and size" /></p>
<p>The hard disk should now be created and you should find yourself back in the VirtualBox Manager. You should be able to see your newly created virtual machine listed on the left.</p>
<h2 id="install-ubuntu-server-in-the-virtual-machine">Install Ubuntu Server in the Virtual Machine</h2>
<p>Make sure your virtual machine is selected and press <em>Start</em>. VirtualBox Manager will ask you to select a virtual optical disk file or a physical optical drive to start the virtual machine from. Click on the folder with the upwards arrow on the right side of the dialogue, select the ISO file you downloaded previously and press <em>Start</em>.</p>
<p><img src="https://res.cloudinary.com/hibbard/image/upload/v1576058634/ubuntu-22.04-virtualbox-6.1.32/virtualbox-manager-8.png" alt="Select start-up disk" /></p>
<p>The Ubuntu installation process will now begin. It consists of multiple steps and is quite painless.</p>
<h3 id="the-welcome-screen">The Welcome Screen</h3>
<p>Here you should select your preferred language. I’m using English.</p>
<p><img src="https://res.cloudinary.com/hibbard/image/upload/v1576058632/ubuntu-22.04-virtualbox-6.1.32/ubuntu-install-1.png" alt="The Welcome Screen" class="terminal" /></p>
<h3 id="installer-update">Installer Update</h3>
<p>If an installer update is available, you can choose to install it or ignore it. I chose to install the update.</p>
<p><img src="https://res.cloudinary.com/hibbard/image/upload/v1576058632/ubuntu-22.04-virtualbox-6.1.32/ubuntu-install-2.png" alt="The Installer Update Screen" class="terminal" /></p>
<p>This will download the update, then restart the installer.</p>
<h3 id="keyboard-configuration">Keyboard Configuration</h3>
<p>Here you should select a keyboard layout. As I’m using a German keyboard, I asked Ubuntu to detect my layout, which it did with a couple of simple questions.</p>
<p><img src="https://res.cloudinary.com/hibbard/image/upload/v1576058632/ubuntu-22.04-virtualbox-6.1.32/ubuntu-install-3.png" alt="The Keyboard Configuration Screen" class="terminal" /></p>
<h3 id="type-of-install">Type of Install</h3>
<p>Next you should choose between the default install that contains a curated set of packages and a minimized version, which has been customized to have a small runtime footprint. Choose the default option.</p>
<p><img src="https://res.cloudinary.com/hibbard/image/upload/v1576058632/ubuntu-22.04-virtualbox-6.1.32/ubuntu-install-4.png" alt="The Type of Install Screen" class="terminal" /></p>
<h3 id="network-connections">Network Connections</h3>
<p>Here Ubuntu will attempt to configure the standard network interface. Normally you can just accept the default and select <em>Done</em>.</p>
<p><img src="https://res.cloudinary.com/hibbard/image/upload/v1576058632/ubuntu-22.04-virtualbox-6.1.32/ubuntu-install-5.png" alt="The Network Connections Screen" class="terminal" /></p>
<h3 id="configure-proxy">Configure Proxy</h3>
<p>If your system requires a proxy to connect to the internet (mine doesn’t), enter its details in the next dialogue. Then select <em>Done</em>.</p>
<p><img src="https://res.cloudinary.com/hibbard/image/upload/v1576058632/ubuntu-22.04-virtualbox-6.1.32/ubuntu-install-6.png" alt="The Configure Proxy Screen" class="terminal" /></p>
<h3 id="configure-ubuntu-archive-mirror">Configure Ubuntu Archive Mirror</h3>
<p>If you wish to use an alternative mirror for Ubuntu, you can enter the details here. Otherwise accept the default mirror by selecting <em>Done</em>. I accepted the default.</p>
<p><img src="https://res.cloudinary.com/hibbard/image/upload/v1576058632/ubuntu-22.04-virtualbox-6.1.32/ubuntu-install-7.png" alt="The Ubuntu Archive Mirror Screen" class="terminal" /></p>
<h3 id="guided-storage-configuration">Guided Storage Configuration</h3>
<p>The installer can guide you through partitioning an entire disk or, if you prefer, you can do it manually. If you choose to partition an entire disk you will still have a chance to review and modify the results before Ubuntu is installed. I selected <em>Use An Entire Disk</em>.</p>
<p>You can optionally instruct the installer to set up the disk as an LVM group, as well as to encrypt it using LUKS. I chose to go with the LVM setup, as LVM offers a number of benifits, such as allowing easier backups of a running server. You can read more about LVM here: <a href="https://askubuntu.com/q/3596/645148">What is LVM and what is it used for?</a></p>
<p><img src="https://res.cloudinary.com/hibbard/image/upload/v1576058632/ubuntu-22.04-virtualbox-6.1.32/ubuntu-install-8.png" alt="The Guided Storage Configuration Screen" class="terminal" /></p>
<h3 id="storage-configuration-summary">Storage Configuration Summary</h3>
<p>The next screen summarizes the choices you made in the previous step. If you are happy with everything, select <em>Done</em>.</p>
<p><img src="https://res.cloudinary.com/hibbard/image/upload/v1576058632/ubuntu-22.04-virtualbox-6.1.32/ubuntu-install-9.png" alt="The Storage Configuration Summary" class="terminal" /></p>
<p>As this is a “destructive action”. I was asked to confirm my choice with <em>Continue</em>.</p>
<p><img src="https://res.cloudinary.com/hibbard/image/upload/v1576058632/ubuntu-22.04-virtualbox-6.1.32/ubuntu-install-10.png" alt="The Confirm Destructive Action Screen" class="terminal" /></p>
<h3 id="profile-setup">Profile Setup</h3>
<p>Here you are required to enter:</p>
<ul>
<li>Your (real) name</li>
<li>Your server’s name</li>
<li>Your username</li>
<li>Password</li>
</ul>
<p>Fill these details out as you see fit.</p>
<p><img src="https://res.cloudinary.com/hibbard/image/upload/v1576058633/ubuntu-22.04-virtualbox-6.1.32/ubuntu-install-11.png" alt="The Profile Setup Screen" class="terminal" /></p>
<h3 id="ssh-setup">SSH Setup</h3>
<p>Here we have a chance to install the <a href="https://ubuntu.com/server/docs/service-openssh">OpenSSH server package</a>. We’ll need this to connect to the virtual machine via SSH later on, so ensure that you select it.</p>
<p>You also have the opportunity to import your SSH keys from GitHub or Launchpad. I selected <em>No</em> for this option.</p>
<p><img src="https://res.cloudinary.com/hibbard/image/upload/v1576058633/ubuntu-22.04-virtualbox-6.1.32/ubuntu-install-12.png" alt="The SSH Setup Screen" class="terminal" /></p>
<h3 id="featured-server-snaps">Featured Server Snaps</h3>
<p>Here you can select from a list of popular snaps to install on your system. <a href="https://tutorials.ubuntu.com/tutorial/basic-snap-usage#0">Snaps</a> are self-contained software packages that work across a range of Linux distributions. I didn’t select any.</p>
<p><img src="https://res.cloudinary.com/hibbard/image/upload/v1576058633/ubuntu-22.04-virtualbox-6.1.32/ubuntu-install-13.png" alt="The Featured Server Snaps screen" class="terminal" /></p>
<h3 id="install-and-reboot">Install and reboot</h3>
<p>And that’s it, the installaler will now install Ubuntu 22.04. Once it is finished you should select <em>Reboot Now</em></p>
<p><img src="https://res.cloudinary.com/hibbard/image/upload/v1576058633/ubuntu-22.04-virtualbox-6.1.32/ubuntu-install-14.png" alt="The Reboot screen" class="terminal" /></p>
<p>Ubuntu will ask you to remove the installation medium and press <kbd>Enter</kbd>. You can remove the disk via_Devices_ > <em>Optical Drives</em> > <em>Remove disk from virtual drive</em>. You will need to put a check mark next to <em>ubuntu-22.04-live-server-amd64.iso</em> if it is not selected already.</p>
<h2 id="up-and-running-with-ssh">Up and Running with SSH</h2>
<p>Once your virtual machine has rebooted and you have logged in, you’ll probably notice that some packages can be updated.</p>
<p>Let’s fix that:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>apt update
<span class="nb">sudo </span>apt upgrade
</code></pre></div></div>
<p>If you see a <em>Daemons using outdated libraries</em> dialogue asking you which services should be restarted, just accept the defaults and navigate to <em>OK</em> by pressing <kbd>TAB</kbd>.</p>
<p>Now let’s double check that SSH is installed (it should be if you selected the option <em>Install OpenSSH server</em> during instalation).</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>jim@odin:~<span class="nv">$ </span>ssh
usage: ssh <span class="o">[</span><span class="nt">-46AaCfGgKkMNnqsTtVvXxYy</span><span class="o">]</span> <span class="o">[</span><span class="nt">-b</span> bind_address] <span class="o">[</span><span class="nt">-c</span> cipher_spec]
<span class="o">[</span><span class="nt">-D</span> <span class="o">[</span>bind_address:]port] <span class="o">[</span><span class="nt">-E</span> log_file] <span class="o">[</span><span class="nt">-e</span> escape_char]
<span class="o">[</span><span class="nt">-F</span> configfile] <span class="o">[</span><span class="nt">-I</span> pkcs11] <span class="o">[</span><span class="nt">-i</span> identity_file]
<span class="o">[</span><span class="nt">-J</span> <span class="o">[</span>user@]host[:port]] <span class="o">[</span><span class="nt">-L</span> address] <span class="o">[</span><span class="nt">-l</span> login_name] <span class="o">[</span><span class="nt">-m</span> mac_spec]
<span class="o">[</span><span class="nt">-O</span> ctl_cmd] <span class="o">[</span><span class="nt">-o</span> option] <span class="o">[</span><span class="nt">-p</span> port] <span class="o">[</span><span class="nt">-Q</span> query_option] <span class="o">[</span><span class="nt">-R</span> address]
<span class="o">[</span><span class="nt">-S</span> ctl_path] <span class="o">[</span><span class="nt">-W</span> host:port] <span class="o">[</span><span class="nt">-w</span> local_tun[:remote_tun]]
<span class="o">[</span>user@]hostname <span class="o">[</span><span class="nb">command</span><span class="o">]</span>
</code></pre></div></div>
<p>If you get a “command not found” error, you can install it with:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>sudo apt-get install openssh-server
</code></pre></div></div>
<p>The next step is to give our Ubuntu server an IP address on our local network. To do this, power off the virtual machine using <code class="language-plaintext highlighter-rouge">sudo poweroff</code> or <em>Machine</em> > <em>ACPI Shutdown</em>.</p>
<p>Then, in VirtualBox Manager, make sure your machine is selected and click <em>Settings</em>. Click on <em>Network</em> on the left and change the setting <em>Adapter 1</em> > NAT_ to <em>“_Bridged Adapter</em>“_ and click <em>OK</em>.</p>
<p><img src="https://res.cloudinary.com/hibbard/image/upload/v1576058634/ubuntu-22.04-virtualbox-6.1.32/virtualbox-manager-9.png" alt="VirtualBox Manager - Network Adapter 1" /></p>
<p>Start up your virtual machine, then enter <code class="language-plaintext highlighter-rouge">ip address</code> (in the guest) and note the IP address assigned to your main network adapter. In my case this was <code class="language-plaintext highlighter-rouge">192.168.178.40</code>.</p>
<p><strong>Note:</strong> it is also possible to stick with the original NAT interface and SSH into the guest using port forwarding. You can read more about that <a href="https://stackoverflow.com/a/10532299/1136887">here</a>. You can find information on all of the VBox network settings <a href="https://www.nakivo.com/blog/virtualbox-network-setting-guide/">in this comprehensive guide</a>.</p>
<h2 id="starting-and-stopping-virtualbox-in-headless-mode">Starting and Stopping VirtualBox in Headless Mode</h2>
<p>You might have noticed, working with the VirtualBox Manager and the guest OS is a bit of a pain. If you’re going to continue doing this, you should at least <a href="https://www.virtualbox.org/manual/ch04.html">install the guest additions</a>, as well as enable clipboard support.</p>
<p>There is a slightly nicer way however — you can start and stop the virtual machine using the <a href="https://www.virtualbox.org/manual/ch08.html">VBoxManage command</a> from your terminal.</p>
<p>To power on:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>VBoxManage startvm <span class="s2">"Ubuntu Server 22.04"</span> <span class="nt">--type</span> headless
</code></pre></div></div>
<p>And to power off:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>VBoxManage controlvm <span class="s2">"Ubuntu Server 22.04"</span> poweroff
</code></pre></div></div>
<p>Where “Ubuntu Server 22.04” is whatever you called your virtual machine (the name it has in the VirtualBox Manager GUI).</p>
<h2 id="connecting-to-the-ubuntu-server">Connecting to the Ubuntu Server</h2>
<p>Let’s go ahead and start the Ubuntu server in headless mode, before connecting to it via SSH.</p>
<blockquote>
<p>Note: The following commands should be run on your host.</p>
</blockquote>
<p>On most *nix systems, the SSH client software should be part of the default installation. If you don’t have it available, you should be able to grab it from the repos, like so:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>apt <span class="nb">install </span>openssh-client
</code></pre></div></div>
<p>or just hit DuckDuckGo.</p>
<p>Then (ensuring that you replace “jim” and the IP address with your corresponding values) you can connect like so:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ssh jim@192.168.178.40
</code></pre></div></div>
<p>This will give you a warning that the host’s authenticity cannot be established and ask you if you want to continue connecting. Answer “yes”.</p>
<p>Next, it will prompt you for your password. Enter it and you will be connected to your Ubuntu server from your host OS.</p>
<p><img src="https://res.cloudinary.com/hibbard/image/upload/v1576058634/ubuntu-22.04-virtualbox-6.1.32/ubuntu-ssh-1.png" alt="Connected to the Ubuntu server via SSH" class="terminal" /></p>
<h3 id="for-windows-users">For Windows Users</h3>
<p>If you’re running Windows you’ll need to install a <a href="https://www.ssh.com/ssh/putty/download#sec-Download-PuTTY-installation-package-for-Windows">SSH client such as PuTTY</a>.</p>
<p>When PuTTY starts, a window titled <em>PuTTY Configuration</em> should open. This window has a configuration pane on the left, a <em>Host Name</em> field and other options in the middle, and a pane for saving session profiles in the lower right area.</p>
<p>For simple use, all you need to do is to enter the IP address of the host you want to connect to in the <em>Host Name</em> field and click <em>Open</em>.</p>
<p><img src="https://res.cloudinary.com/hibbard/image/upload/v1576058633/ubuntu-22.04-virtualbox-6.1.32/ubuntu-putty-1.png" alt="PuTTY Configuration" /></p>
<h2 id="generate-and-install-a-ssh-key-pair">Generate and Install a SSH Key Pair</h2>
<p>SSH keys offer a secure manner of logging into a server without the need of a password.</p>
<p>In a nutshell, this depends upon you generating a public and a private SSH key pair. The private key is kept on your PC (and should be guarded carefully). The public key is copied over to the server you wish to connect to.</p>
<p>SSH keys are a complex subject and as such, out of the scope of this tutorial. If you’d like to find out more, I recommend looking for a dedicated tutorial (<a href="https://www.howtoforge.com/linux-basics-how-to-install-ssh-keys-on-the-shell">such as this one</a>).</p>
<h3 id="generate-the-keys">Generate the Keys</h3>
<p>On *nix systems (Windows users see the next section), you can generate your key pair with the following command:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ssh-keygen <span class="nt">-o</span> <span class="nt">-b</span> 4096 <span class="nt">-t</span> rsa
</code></pre></div></div>
<p>The <code class="language-plaintext highlighter-rouge">-o</code> option instructs ssh-keygen to store the private key in the new OpenSSH format instead of the old (and more compatible PEM format). This is advisable, as the new OpenSSH format has an increased resistance to brute-force password cracking.</p>
<p>The <code class="language-plaintext highlighter-rouge">-b</code> option is used to set the key length to 4096 bits instead of the default 1024 bits for security reasons.</p>
<p>In the following dialogue you will be required to answer a couple of questions:</p>
<ul>
<li>Where to save the newly generated key pair</li>
<li>Which passphrase to use</li>
</ul>
<p>Here you can accept the default location and leave the passphrase blank by pressing <kbd>Return</kbd>.</p>
<p>ssh-keygen will then output a summary of what it has done:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Generating public/private rsa key pair.
Enter file <span class="k">in </span>which to save the key <span class="o">(</span>/home/jim/.ssh/id_rsa<span class="o">)</span>:
Created directory <span class="s1">'/home/jim/.ssh'</span><span class="nb">.</span>
Enter passphrase <span class="o">(</span>empty <span class="k">for </span>no passphrase<span class="o">)</span>:
Enter same passphrase again:
Your identification has been saved <span class="k">in</span> /home/jim/.ssh/id_rsa.
Your public key has been saved <span class="k">in</span> /home/jim/.ssh/id_rsa.pub.
The key fingerprint is:
SHA256:sx5uJeVdH/cT/1+GxsSWYzmjf5hUaE33f/e57EbqBfY jim@fitz
The key<span class="s1">'s randomart image is:
+---[RSA 4096]----+
| |
| o|
| +o|
| . .+==|
| So . =@oB|
| .oo o*+BB|
| oo ..*EX|
| o.. +=+=|
| .o ..+=+|
+----[SHA256]-----+
</span></code></pre></div></div>
<h3 id="copy-the-public-key-to-the-ubuntu-server">Copy the Public Key to the Ubuntu Server</h3>
<p>To copy the public key to the Ubuntu server use:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ssh-copy-id <span class="nt">-i</span> ~/.ssh/id_rsa.pub jim@192.168.178.40
</code></pre></div></div>
<p>Where <code class="language-plaintext highlighter-rouge">~/.ssh/id_rsa.pub</code> is the path to your public key, taken from the output above. And where <code class="language-plaintext highlighter-rouge">jim@192.168.178.40</code> should be altered to reflect your details.</p>
<p>The command will run and you should be asked for your server password. Enter it, then attempt to log into the server like so:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ssh jim@192.168.178.40
</code></pre></div></div>
<p>This time you should be in without a password.</p>
<h2 id="for-windows-users-1">For Windows Users</h2>
<p>You should be able to use a tool like <a href="https://www.ssh.com/ssh/putty/windows/puttygen">PuTTYgen</a> to achieve the same thing. Here is a tutorial on using PuTTYgen to <a href="https://www.ssh.com/ssh/putty/windows/puttygen#sec-Creating-a-new-key-pair-for-authentication">create a new key pair for authentication</a>.</p>
<p>You will have a little more leg work when it comes to copying the key to the server, where you will need to add the public key to a <code class="language-plaintext highlighter-rouge">~/.ssh/authorized_keys</code> file.</p>
<p>You can do that like so (on the guest):</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>cd
mkdir .ssh
cd .ssh
nano authorized_keys
</code></pre></div></div>
<p>This will create the appropriate file, then open the nano editor into which you can copy your newly generated public key.</p>
<p>When you’re done, press <kbd>Ctrl</kbd> + <kbd>X</kbd> to save your changes and exit nano.</p>
<h2 id="conclusion">Conclusion</h2>
<p>This has been quite a long post, but by the end of it you should have a working installation of Ubuntu Server running on VirtualBox that you can connect to from your host operating system via SSH.</p>
<p>If you have any questions or feedback, I’d be glad to hear your from you in the comments.</p>James HibbardIn this post I’ll show you how to install Ubuntu Server 22.04 LTS (Jammy Jellyfish) on Oracle’s VirtualBox. I’ll also demonstrate how to connect to the Ubuntu instance via SSH, as well as how to run VirtualBox in headless mode. Let’s get started!Getting Started with NodeGUI2019-09-28T00:00:00+00:002019-09-28T00:00:00+00:00https://hibbard.eu/getting-started-with-nodegui<p><a href="https://github.com/nodegui/nodegui">NodeGUI</a> is an open source library for building cross-platform, native desktop applications with JavaScript and CSS-like styling.</p>
<p>In this article, I’m going to demonstrate how to get up and running with NodeGUI. We’ll set up a development environment, take a look at several of the library’s basic concepts, then finish off by creating a simple password generator app.</p>
<p>If you’re curious as to what we’ll end up with, <a href="https://github.com/jameshibbard/nodegui-password-generator">the finished code can be found on GitHub</a>.</p>
<!--more-->
<p>And this is what the app will look like:</p>
<p><img src="https://res.cloudinary.com/hibbard/image/upload/v1581442610/node-gui/password-generator.png" alt="Screenshot of app" /></p>
<h2 id="why-not-electron">Why Not Electron?</h2>
<p>Before we get into it, let’s look at why you might want to use NodeGUI, as opposed to one of the more popular Chromium-based solutions, such as <a href="https://electronjs.org/">Electron</a>.</p>
<h3 id="electron-apps-are-bloated">Electron Apps are Bloated</h3>
<p>The <a href="https://news.ycombinator.com/item?id=12119278">main criticism levelled at Electron apps</a>, is that they are bloated and require too much memory. This is because each Electron app ships with a version of the <a href="https://www.chromium.org/Home">Chromium browser</a> and is not in a position to share resources, as native apps would.</p>
<p>For larger apps on a high-powered machine, this is fine. But when it comes to anything I’m likely to write, shipping a whole browser to render my app feels rather like cheating.</p>
<p>NodeGUI on the other hand, is powered by the <a href="https://wiki.qt.io/About_Qt">Qt framework</a>. This means that its widgets are rendered natively and that <em>it does not need to open up a browser instance to render the UI</em>. This makes it CPU/memory efficient and much more suited to my needs.</p>
<h3 id="privacy-concerns">Privacy Concerns</h3>
<p>As mentioned, Electron apps are based on the open-source version of Google Chrome and, privacy-wise, this is not ideal. Google has become unbelievably data hungry in the past few years, and in my opinion Chromium cannot be trusted to not phone home in some way shape or form.</p>
<p>With NodeGUI, this is obviously a non-issue.</p>
<h2 id="why-not-nodegui">Why Not NodeGUI?</h2>
<p>Although NodeGUI is under active development, the project is in its infancy and <a href="https://github.com/master-atul">the maintainer</a> currently advises against using it in production.</p>
<p>You should also be aware that only a subset of Qt’s modules have been implemented so far. This means that you might find yourself in need of a widget that has yet to be ported. What to do in this case is addressed in the <a href="#getting-help-and-contributing">Getting Help and Contributing</a> section of this article.</p>
<p>And there are a couple of issues when it comes to building/distributing the app. These too, are covered <a href="#i-thought-you-said-cross-platform">at the end of the article</a>.</p>
<p>In short, if you’re looking for a polished, turn-key solution, you’re probably better off going with Electron at this time.</p>
<h2 id="setting-up-a-dev-environment">Setting Up a Dev Environment</h2>
<p>With that out of the way, let’s get NodeGUI up and running.</p>
<h3 id="nodejs">Node.js</h3>
<p>NodeGUI requires Node version 12.x or above, so let’s get that installed first.</p>
<p>Either head on over to the <a href="https://nodejs.org/en/download/">project’s home page</a> and download the correct binaries for your system, or use a version manager such as <a href="https://github.com/creationix/nvm">nvm</a>. I would recommend using a version manager where possible, as this will allow you to install different Node versions and switch between them at will. It will also negate a bunch of potential permissions errors.</p>
<p>You can check that the installation process went well by typing <code class="language-plaintext highlighter-rouge">node -v</code> to confirm the version you are running.</p>
<h3 id="additional-dependencies">Additional Dependencies</h3>
<p>I’m running Linux, so the installation instructions in this section will reflect that. For other operating systems, please <a href="https://docs.nodegui.org/docs/guides/getting-started/#developer-environment">check the documentation</a>.</p>
<p>To get NodeGUI working, we’ll need to install <a href="http://man7.org/linux/man-pages/man1/make.1.html">Make</a>, <a href="https://gcc.gnu.org/">GCC</a> v7 and <a href="https://cmake.org/">CMake</a>. This can be done with:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>apt-get <span class="nb">install </span>make gcc cmake
</code></pre></div></div>
<p>On a standard Linux install the chances are that Make and GCC will be installed already. Once done, you can check the versions using:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>make <span class="nt">-v</span>
<span class="nv">$ </span>GNU Make 4.1
gcc <span class="nt">-v</span>
<span class="nv">$ </span>gcc version 7.4.0 <span class="o">(</span>Ubuntu 7.4.0-1ubuntu1~18.04.1<span class="o">)</span>
cmake <span class="nt">--version</span>
<span class="nv">$ </span>cmake version 3.10.2
</code></pre></div></div>
<p>Finally, it is advisable (but probably not essential) to install the <code class="language-plaintext highlighter-rouge">pkg-config</code> and <code class="language-plaintext highlighter-rouge">build-essential</code> packages. You can do this using:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>apt-get <span class="nb">install </span>pkg-config build-essential
</code></pre></div></div>
<p>And that’s it, we’re good to go.</p>
<h2 id="clone-the-starter-repo">Clone the Starter Repo</h2>
<p>Next, let’s clone the <a href="https://github.com/nodegui/nodegui-starter">nodegui-starter repo</a>. This will provide us with the minimal setup we need to start developing.</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git clone https://github.com/nodegui/nodegui-starter
<span class="nb">cd </span>nodegui-starter
npm <span class="nb">install</span>
</code></pre></div></div>
<p>Running <code class="language-plaintext highlighter-rouge">npm install</code> will download a custom Node binary called <a href="https://docs.nodegui.org/docs/guides/nodegui-architecture#qode">Qode</a>, upon which NodeGUI is based. It will then use the tools we installed previously to compile the C++ files that comprise the library.</p>
<p>After this has completed, you can run <code class="language-plaintext highlighter-rouge">npm run start</code> from the project root, to see the canonical “Hello, World!” example. Under the hood, this command will kick off webpack, which transpiles the contents of <code class="language-plaintext highlighter-rouge">src/index.ts</code> to <code class="language-plaintext highlighter-rouge">dist/index.js</code> which is then served up using Qode.</p>
<p>And here’s the result. Pretty, no?</p>
<p><img src="https://res.cloudinary.com/hibbard/image/upload/v1569312118/node-gui/node-gui-01.png" alt="Hello, World!" /></p>
<h2 id="examining-the-demo-app">Examining the Demo App</h2>
<p>Before moving on to building something ourselves, let’s have a look at the demo app to highlight a couple of NodeGUI concepts.</p>
<p>Open <code class="language-plaintext highlighter-rouge">src/index.ts</code> in your editor of choice — as it’s a TypeScript file, you might want to install syntax highlighting if you haven’t already. For Sublime Text, I use the <a href="https://github.com/braver/TypeScriptSyntax">TypeScriptSyntax package</a>.</p>
<p>Don’t worry if you haven’t any experience with TypeScript. Although NodeGUI offers first class TypeScript support, it also works just fine with regular JavaScript. The password generator that we will build later on will be written in JavaScript, not TypeScript.</p>
<h3 id="import-the-widgets-you-need">Import the Widgets You Need</h3>
<p>The first few lines of <code class="language-plaintext highlighter-rouge">index.ts</code> look like this:</p>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="p">{</span>
<span class="nx">QMainWindow</span><span class="p">,</span>
<span class="nx">QWidget</span><span class="p">,</span>
<span class="nx">QLabel</span><span class="p">,</span>
<span class="nx">FlexLayout</span>
<span class="p">}</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="dl">"</span><span class="s2">@nodegui/nodegui</span><span class="dl">"</span><span class="p">);</span>
</code></pre></div></div>
<p>All this does is import the necessary modules for the GUI.</p>
<p>You can find a <a href="https://docs.nodegui.org/docs/api/manual/synopsis">list of all available modules</a> in the project’s excellent documentation. This list is steadily growing and if you ever find yourself in need of modules or functionality which hasn’t yet been implemented, you can <a href="https://github.com/nodegui/nodegui/issues">open an issue in the NodeGUI repo</a> to ask if/when it might be added.</p>
<h3 id="creating-a-window-and-adding-a-layout">Creating a Window and Adding a Layout</h3>
<p>The following lines create a main application window and display it. Every widget in NodeGUI should be a child, or nested child of <a href="https://docs.nodegui.org/docs/api/generated/classes/qmainwindow">QMainWindow</a>.</p>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">win</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">QMainWindow</span><span class="p">();</span>
<span class="p">...</span>
<span class="nx">win</span><span class="p">.</span><span class="nx">show</span><span class="p">();</span>
<span class="p">(</span><span class="nb">global</span> <span class="k">as</span> <span class="nx">any</span><span class="p">).</span><span class="nx">win</span> <span class="o">=</span> <span class="nx">win</span><span class="p">;</span>
</code></pre></div></div>
<p>The final line is the only bit of TypeScript in the file. It compiles to <code class="language-plaintext highlighter-rouge">global.win = win;</code> in JavaScript. The purpose of this line is to prevent the garbage collection of <code class="language-plaintext highlighter-rouge">win</code>, which would otherwise see the window disappear after a few minutes.</p>
<p>Before any widgets can be added to the main application window, it needs to have a central widget set. This is a <a href="https://docs.nodegui.org/docs/api/generated/classes/qwidget">QWidget</a> which can be used to encapsulate other widgets and provide structure. It has a similar role to that of a div in the web world.</p>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">centralWidget</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">QWidget</span><span class="p">();</span>
<span class="p">...</span>
<span class="nx">win</span><span class="p">.</span><span class="nx">setCentralWidget</span><span class="p">(</span><span class="nx">centralWidget</span><span class="p">);</span>
</code></pre></div></div>
<p>One of my favourite features of NodeGUI is that it has full support for flexbox layout. To add this to the main application window, the <a href="https://docs.nodegui.org/docs/api/generated/classes/flexlayout">FlexLayout</a> module is used.</p>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">rootLayout</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">FlexLayout</span><span class="p">();</span>
<span class="nx">centralWidget</span><span class="p">.</span><span class="nx">setLayout</span><span class="p">(</span><span class="nx">rootLayout</span><span class="p">);</span>
</code></pre></div></div>
<p>If you’re unfamiliar with flexbox, check out this <a href="https://www.sitepoint.com/flexbox-css-flexible-box-layout/">friendly introduction over on SitePoint</a> to get up to speed quickly.</p>
<h3 id="adding-child-widgets-and-styling">Adding Child Widgets and Styling</h3>
<p>The demo app uses two child widgets to display a greeting. The first of these looks like so:</p>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">label</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">QLabel</span><span class="p">();</span>
<span class="p">...</span>
<span class="nx">label</span><span class="p">.</span><span class="nx">setText</span><span class="p">(</span><span class="dl">"</span><span class="s2">Hello</span><span class="dl">"</span><span class="p">);</span>
<span class="p">...</span>
<span class="nx">rootLayout</span><span class="p">.</span><span class="nx">addWidget</span><span class="p">(</span><span class="nx">label</span><span class="p">);</span>
</code></pre></div></div>
<p>Nothing too exciting happening here. A <a href="https://docs.nodegui.org/docs/api/generated/classes/qlabel">QLabel widget</a> is created and the text “Hello” is added to it. So that the widget will display in the app, it must be added to the FlexLayout mentioned above.</p>
<p>The second widget is slightly more interesting. It’s mostly the same as the first, but the <code class="language-plaintext highlighter-rouge">setInlineStyle</code> method is used to apply some styling.</p>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">label2</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">QLabel</span><span class="p">();</span>
<span class="nx">label2</span><span class="p">.</span><span class="nx">setText</span><span class="p">(</span><span class="dl">"</span><span class="s2">World</span><span class="dl">"</span><span class="p">);</span>
<span class="nx">label2</span><span class="p">.</span><span class="nx">setInlineStyle</span><span class="p">(</span><span class="s2">`
color: red;
`</span><span class="p">);</span>
<span class="p">...</span>
<span class="nx">rootLayout</span><span class="p">.</span><span class="nx">addWidget</span><span class="p">(</span><span class="nx">label2</span><span class="p">);</span>
</code></pre></div></div>
<p>The <a href="https://docs.nodegui.org/docs/api/generated/classes/nodewidget#setstylesheet">setStyleSheet method</a> can also be used to style widgets. For this to work, the widgets need to be assigned an object name using the <a href="https://docs.nodegui.org/docs/api/generated/classes/nodewidget#setobjectname">setObjectName method</a>. Object names are similar to IDs in the web world and allow widgets to be targeted with style rules.</p>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">centralWidget</span><span class="p">.</span><span class="nx">setObjectName</span><span class="p">(</span><span class="dl">"</span><span class="s2">myroot</span><span class="dl">"</span><span class="p">);</span>
<span class="p">...</span>
<span class="nx">label</span><span class="p">.</span><span class="nx">setObjectName</span><span class="p">(</span><span class="dl">"</span><span class="s2">mylabel</span><span class="dl">"</span><span class="p">);</span>
<span class="p">...</span>
<span class="nx">win</span><span class="p">.</span><span class="nx">setStyleSheet</span><span class="p">(</span>
<span class="s2">`
#myroot {
background-color: #009688;
height: '100%';
align-items: 'center';
justify-content: 'center';
}
#mylabel {
font-size: 16px;
font-weight: bold;
}
`</span>
<span class="p">);</span>
</code></pre></div></div>
<p>As you can see, the items in the central widget are being aligned using the <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/align-items">align-items</a> and <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/justify-content">justify-content</a> properties.</p>
<p>And that’s the demo app. Take a while to make sure you understand the code before moving on to the next section.</p>
<h2 id="building-a-password-generator">Building a Password Generator</h2>
<p>Now that we’re familiar with some NodeGUI concepts, let’s turn our hand to building something more interesting, namely a password generator. This app should allow a user to enter a password length, then generate a random password of said length. The user should also be able to specify if the password should contain special characters or not.</p>
<p>In the following sections we’ll create this app step by step, however please be aware that you can grab the completed code from <a href="https://github.com/jameshibbard/nodegui-password-generator">the accompanying GitHub repo</a>.</p>
<h3 id="create-an-app-skeleton">Create an App Skeleton</h3>
<p>As mentioned above, I’ll be using JavaScript and not TypeScript to create the app. Consequently rename <code class="language-plaintext highlighter-rouge">src/index.ts</code> to <code class="language-plaintext highlighter-rouge">src/index.js</code> and ensure it contains the following code:</p>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="p">{</span>
<span class="nx">QMainWindow</span><span class="p">,</span>
<span class="nx">QWidget</span><span class="p">,</span>
<span class="p">}</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="dl">'</span><span class="s1">@nodegui/nodegui</span><span class="dl">'</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">win</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">QMainWindow</span><span class="p">();</span>
<span class="nx">win</span><span class="p">.</span><span class="nx">setWindowTitle</span><span class="p">(</span><span class="dl">'</span><span class="s1">Password Generator</span><span class="dl">'</span><span class="p">);</span>
<span class="nx">win</span><span class="p">.</span><span class="nx">resize</span><span class="p">(</span><span class="mi">400</span><span class="p">,</span> <span class="mi">200</span><span class="p">);</span>
<span class="c1">// Root view</span>
<span class="kd">const</span> <span class="nx">rootView</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">QWidget</span><span class="p">();</span>
<span class="nx">win</span><span class="p">.</span><span class="nx">setCentralWidget</span><span class="p">(</span><span class="nx">rootView</span><span class="p">);</span>
<span class="nx">win</span><span class="p">.</span><span class="nx">show</span><span class="p">();</span>
<span class="nb">global</span><span class="p">.</span><span class="nx">win</span> <span class="o">=</span> <span class="nx">win</span><span class="p">;</span>
</code></pre></div></div>
<p>There shouldn’t be anything surprising here. We are creating a main application window and setting its title and size. We are then creating a root view and setting it to be our central widget, so that we can add child widgets in the next step.</p>
<p>If you run <code class="language-plaintext highlighter-rouge">npm run start</code> in the root directory, this is what you should see.</p>
<p><img src="https://res.cloudinary.com/hibbard/image/upload/v1569323271/node-gui/node-gui-02.png" alt="The app skeleton" /></p>
<h3 id="designing-an-app-layout">Designing an App Layout</h3>
<p>Next we need to consider how the app will be structured. To make things easier to visualize, I’ve created a diagram depicting the layout.</p>
<p>The widgets are numbered 1-10. They specify the widget type, as well as the variable name by which they will be referenced.</p>
<p><img src="https://res.cloudinary.com/hibbard/image/upload/v1569330726/node-gui/node-gui-03.png" alt="App layout" /></p>
<p>This breaks down as follows:</p>
<ol>
<li>Main application window</li>
<li>Main window’s central widget</li>
<li>Container for password options</li>
<li>Container for first row of password options</li>
<li>Label and text input to enter desired password length</li>
<li>Checkbox to specify if special characters should be used</li>
<li>Text area to display generated password</li>
<li>Container for buttons</li>
<li>Button to generate a password</li>
<li>Button to copy the generated password to the clipboard</li>
</ol>
<p>Now let’s translate that to code. We’ll start off with password options.</p>
<blockquote>
<p><strong>Note</strong>: <code class="language-plaintext highlighter-rouge">...</code> denotes code mentioned in previous sections, which I don’t intend to repeat every time. If you would like to compare with the finished file, please <a href="https://github.com/jameshibbard/nodegui-password-generator/blob/master/src/index.js">check here</a>.</p>
</blockquote>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="p">{</span>
<span class="p">...</span>
<span class="nx">FlexLayout</span><span class="p">,</span>
<span class="nx">QCheckBox</span><span class="p">,</span>
<span class="nx">QLabel</span><span class="p">,</span>
<span class="nx">QLineEdit</span><span class="p">,</span>
<span class="p">}</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="dl">'</span><span class="s1">@nodegui/nodegui</span><span class="dl">'</span><span class="p">);</span>
<span class="p">...</span>
<span class="c1">// Root view</span>
<span class="p">...</span>
<span class="kd">const</span> <span class="nx">rootViewLayout</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">FlexLayout</span><span class="p">();</span>
<span class="nx">rootView</span><span class="p">.</span><span class="nx">setObjectName</span><span class="p">(</span><span class="dl">'</span><span class="s1">rootView</span><span class="dl">'</span><span class="p">);</span>
<span class="nx">rootView</span><span class="p">.</span><span class="nx">setLayout</span><span class="p">(</span><span class="nx">rootViewLayout</span><span class="p">);</span>
<span class="c1">// Fieldset</span>
<span class="kd">const</span> <span class="nx">fieldset</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">QWidget</span><span class="p">();</span>
<span class="kd">const</span> <span class="nx">fieldsetLayout</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">FlexLayout</span><span class="p">();</span>
<span class="nx">fieldset</span><span class="p">.</span><span class="nx">setObjectName</span><span class="p">(</span><span class="dl">'</span><span class="s1">fieldset</span><span class="dl">'</span><span class="p">);</span>
<span class="nx">fieldset</span><span class="p">.</span><span class="nx">setLayout</span><span class="p">(</span><span class="nx">fieldsetLayout</span><span class="p">);</span>
<span class="c1">// Number characters row</span>
<span class="kd">const</span> <span class="nx">numCharsRow</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">QWidget</span><span class="p">();</span>
<span class="kd">const</span> <span class="nx">numCharsRowLayout</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">FlexLayout</span><span class="p">();</span>
<span class="nx">numCharsRow</span><span class="p">.</span><span class="nx">setObjectName</span><span class="p">(</span><span class="dl">'</span><span class="s1">numCharsRow</span><span class="dl">'</span><span class="p">);</span>
<span class="nx">numCharsRow</span><span class="p">.</span><span class="nx">setLayout</span><span class="p">(</span><span class="nx">numCharsRowLayout</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">numCharsLabel</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">QLabel</span><span class="p">();</span>
<span class="nx">numCharsLabel</span><span class="p">.</span><span class="nx">setText</span><span class="p">(</span><span class="dl">'</span><span class="s1">Number of characters in the password:</span><span class="dl">'</span><span class="p">);</span>
<span class="nx">numCharsRowLayout</span><span class="p">.</span><span class="nx">addWidget</span><span class="p">(</span><span class="nx">numCharsLabel</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">numCharsInput</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">QLineEdit</span><span class="p">();</span>
<span class="nx">numCharsInput</span><span class="p">.</span><span class="nx">setObjectName</span><span class="p">(</span><span class="dl">'</span><span class="s1">numCharsInput</span><span class="dl">'</span><span class="p">);</span>
<span class="nx">numCharsRowLayout</span><span class="p">.</span><span class="nx">addWidget</span><span class="p">(</span><span class="nx">numCharsInput</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">checkbox</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">QCheckBox</span><span class="p">();</span>
<span class="nx">checkbox</span><span class="p">.</span><span class="nx">setText</span><span class="p">(</span><span class="dl">'</span><span class="s1">Include special characters in password</span><span class="dl">'</span><span class="p">);</span>
<span class="c1">// Add the widgets to the respective layouts</span>
<span class="nx">fieldsetLayout</span><span class="p">.</span><span class="nx">addWidget</span><span class="p">(</span><span class="nx">numCharsRow</span><span class="p">);</span>
<span class="nx">fieldsetLayout</span><span class="p">.</span><span class="nx">addWidget</span><span class="p">(</span><span class="nx">checkbox</span><span class="p">);</span>
<span class="nx">rootViewLayout</span><span class="p">.</span><span class="nx">addWidget</span><span class="p">(</span><span class="nx">fieldset</span><span class="p">);</span>
<span class="c1">// Styling</span>
<span class="kd">const</span> <span class="nx">rootStyleSheet</span> <span class="o">=</span> <span class="s2">`
#rootView {
padding: 5px;
}
#fieldset {
padding: 10px;
border: 2px ridge #bdbdbd;
margin-bottom: 4px;
}
#numCharsRow, #buttonRow {
flex-direction: row;
}
#numCharsRow {
margin-bottom: 5px;
}
#numCharsInput {
width: 40px;
margin-left: 2px;
}
`</span><span class="p">;</span>
<span class="nx">rootView</span><span class="p">.</span><span class="nx">setStyleSheet</span><span class="p">(</span><span class="nx">rootStyleSheet</span><span class="p">);</span>
<span class="p">...</span>
</code></pre></div></div>
<p>The main thing to notice here is that for each <code class="language-plaintext highlighter-rouge">QWidget</code> (which, remember, is like a div element in web terms), we create a new <code class="language-plaintext highlighter-rouge">FlexLayout</code>. This allows us to lay out the <code class="language-plaintext highlighter-rouge">QWidget</code>’s child widgets using flexbox.</p>
<p>Notice also how we use the <code class="language-plaintext highlighter-rouge">addWidget</code> method to add the widgets to their respective layouts.</p>
<p>If at this point you run the app using <code class="language-plaintext highlighter-rouge">npm run start</code>, you should see:</p>
<p><img src="https://res.cloudinary.com/hibbard/image/upload/v1569332843/node-gui/node-gui-04.png" alt="App displaying password generation options" /></p>
<p>Next, let’s add the widget to display the generated password. We’ll use a <a href="https://docs.nodegui.org/docs/api/generated/classes/qplaintextedit">QPlainTextEdit</a> widget for the purpose. Let’s start by requiring it:</p>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="p">{</span>
<span class="p">...</span>
<span class="nx">QPlainTextEdit</span><span class="p">,</span>
<span class="p">}</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="dl">'</span><span class="s1">@nodegui/nodegui</span><span class="dl">'</span><span class="p">);</span>
</code></pre></div></div>
<p>Next, create the layout:</p>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Generated password output</span>
<span class="kd">const</span> <span class="nx">passOutput</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">QPlainTextEdit</span><span class="p">();</span>
<span class="nx">passOutput</span><span class="p">.</span><span class="nx">setObjectName</span><span class="p">(</span><span class="dl">'</span><span class="s1">passOutput</span><span class="dl">'</span><span class="p">);</span>
<span class="nx">passOutput</span><span class="p">.</span><span class="nx">setReadOnly</span><span class="p">(</span><span class="kc">true</span><span class="p">);</span>
<span class="nx">passOutput</span><span class="p">.</span><span class="nx">setWordWrapMode</span><span class="p">(</span><span class="mi">3</span><span class="p">);</span>
</code></pre></div></div>
<p>Notice that we make it read only and that we set its word wrap mode to <code class="language-plaintext highlighter-rouge">3</code>. This will prevent the widget attempting to insert line breaks into the password when it contains special characters.</p>
<p>Finally, add the widget to the root view layout and add some styling.</p>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Add the widgets to the respective layouts</span>
<span class="p">...</span>
<span class="nx">rootViewLayout</span><span class="p">.</span><span class="nx">addWidget</span><span class="p">(</span><span class="nx">passOutput</span><span class="p">);</span>
<span class="c1">// Styling</span>
<span class="kd">const</span> <span class="nx">rootStyleSheet</span> <span class="o">=</span> <span class="s2">`
...
#passOutput {
height: 85px;
margin-bottom: 4px;
}
`</span><span class="p">;</span>
</code></pre></div></div>
<p>Now when you run the app, you should see this:</p>
<p><img src="https://res.cloudinary.com/hibbard/image/upload/v1569333478/node-gui/node-gui-05.png" alt="App with widget to display generated password" /></p>
<p>Finally, let’s add the buttons to generate the password and copy it to the clipboard. For this we’ll use the <a href="https://docs.nodegui.org/docs/api/generated/classes/qpushbutton">QPushButton</a> widget.</p>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="p">{</span>
<span class="p">...</span>
<span class="nx">QPushButton</span><span class="p">,</span>
<span class="p">}</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="dl">'</span><span class="s1">@nodegui/nodegui</span><span class="dl">'</span><span class="p">);</span>
<span class="p">...</span>
<span class="c1">// Button row</span>
<span class="kd">const</span> <span class="nx">buttonRow</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">QWidget</span><span class="p">();</span>
<span class="kd">const</span> <span class="nx">buttonRowLayout</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">FlexLayout</span><span class="p">();</span>
<span class="nx">buttonRow</span><span class="p">.</span><span class="nx">setLayout</span><span class="p">(</span><span class="nx">buttonRowLayout</span><span class="p">);</span>
<span class="nx">buttonRow</span><span class="p">.</span><span class="nx">setObjectName</span><span class="p">(</span><span class="dl">'</span><span class="s1">buttonRow</span><span class="dl">'</span><span class="p">);</span>
<span class="c1">// Buttons</span>
<span class="kd">const</span> <span class="nx">generateButton</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">QPushButton</span><span class="p">();</span>
<span class="nx">generateButton</span><span class="p">.</span><span class="nx">setText</span><span class="p">(</span><span class="dl">'</span><span class="s1">Generate</span><span class="dl">'</span><span class="p">);</span>
<span class="nx">generateButton</span><span class="p">.</span><span class="nx">setObjectName</span><span class="p">(</span><span class="dl">'</span><span class="s1">generateButton</span><span class="dl">'</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">copyButton</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">QPushButton</span><span class="p">();</span>
<span class="nx">copyButton</span><span class="p">.</span><span class="nx">setText</span><span class="p">(</span><span class="dl">'</span><span class="s1">Copy to clipboard</span><span class="dl">'</span><span class="p">);</span>
<span class="c1">// Add the widgets to the respective layouts</span>
<span class="p">...</span>
<span class="nx">buttonRowLayout</span><span class="p">.</span><span class="nx">addWidget</span><span class="p">(</span><span class="nx">generateButton</span><span class="p">);</span>
<span class="nx">buttonRowLayout</span><span class="p">.</span><span class="nx">addWidget</span><span class="p">(</span><span class="nx">copyButton</span><span class="p">);</span>
<span class="nx">rootViewLayout</span><span class="p">.</span><span class="nx">addWidget</span><span class="p">(</span><span class="nx">buttonRow</span><span class="p">);</span>
<span class="c1">// Styling</span>
<span class="kd">const</span> <span class="nx">rootStyleSheet</span> <span class="o">=</span> <span class="s2">`
...
#buttonRow{
margin-bottom: 5px;
}
#generateButton {
width: 120px;
margin-right: 3px;
}
#copyButton {
width: 120px;
}
`</span><span class="p">;</span>
</code></pre></div></div>
<p>This process should be familiar by now: require the widget, create a layout, add the widget to the layout, then add some styling.</p>
<p>Now when you run the app, it should look like this:</p>
<p><img src="https://res.cloudinary.com/hibbard/image/upload/v1569333948/node-gui/node-gui-06.png" alt="The finished layout" /></p>
<p>If you’re seeing anything different, check <a href="https://github.com/jameshibbard/nodegui-password-generator/blob/master/src/index.js">index.js on GitHub</a> to find out where you have gone wrong.</p>
<h2 id="adding-functionality-to-the-app">Adding Functionality to the App</h2>
<p>Now we have our layout done, it’s time to make the app do something. Let’s start off by attaching an event listener to the <em>Generate</em> button, which will log whatever value the user has entered.</p>
<p>To make the buttons do anything, we’re going to need the <code class="language-plaintext highlighter-rouge">QPushButtonEvents</code> module.</p>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="p">{</span>
<span class="p">...</span>
<span class="nx">QPushButtonEvents</span><span class="p">,</span>
<span class="p">}</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="dl">'</span><span class="s1">@nodegui/nodegui</span><span class="dl">'</span><span class="p">);</span>
<span class="c1">// Event handling</span>
<span class="nx">generateButton</span><span class="p">.</span><span class="nx">addEventListener</span><span class="p">(</span><span class="nx">QPushButtonEvents</span><span class="p">.</span><span class="nx">clicked</span><span class="p">,</span> <span class="p">()</span> <span class="o">=></span> <span class="p">{</span>
<span class="kd">const</span> <span class="nx">passwordLength</span> <span class="o">=</span> <span class="nx">numCharsInput</span><span class="p">.</span><span class="nx">text</span><span class="p">();</span>
<span class="kd">const</span> <span class="nx">includeSpecialChars</span> <span class="o">=</span> <span class="nx">checkbox</span><span class="p">.</span><span class="nx">isChecked</span><span class="p">();</span>
<span class="nx">passOutput</span><span class="p">.</span><span class="nx">setPlainText</span><span class="p">(</span><span class="s2">`
You entered: </span><span class="p">${</span><span class="nx">passwordLength</span><span class="p">}</span><span class="s2">
Special characters: </span><span class="p">${</span><span class="nx">includeSpecialChars</span><span class="p">?</span> <span class="dl">'</span><span class="s1">yes</span><span class="dl">'</span> <span class="p">:</span> <span class="dl">'</span><span class="s1">no</span><span class="dl">'</span><span class="p">}</span><span class="s2">
`</span><span class="p">);</span>
<span class="p">});</span>
</code></pre></div></div>
<p>As you can see, we can access the value of the <code class="language-plaintext highlighter-rouge">QLineEdit</code> using its <a href="https://docs.nodegui.org/docs/api/generated/classes/qlineedit#text">text</a> method and the value of the <code class="language-plaintext highlighter-rouge">QCheckbox</code> with its <a href="https://docs.nodegui.org/docs/api/generated/classes/qcheckbox#ischecked">isChecked</a> method. We can also use the <code class="language-plaintext highlighter-rouge">QPlainTextEdit</code>’s <a href="https://docs.nodegui.org/docs/api/generated/classes/qplaintextedit#setplaintext">setPlainText</a> method to set this widget’s value.</p>
<p>Now when you run the app, enter something and press <em>Generate</em>, you should see the values you enter logged to the password field.</p>
<p><img src="https://res.cloudinary.com/hibbard/image/upload/v1569335299/node-gui/node-gui-07.png" alt="App displaying user input" /></p>
<h2 id="generating-passwords">Generating Passwords</h2>
<p>Finally we come to generating passwords, which is after all, the purpose of the app.</p>
<p>The way this will work is that we will declare thee arrays representing the character sets, <code class="language-plaintext highlighter-rouge">a-z</code>, <code class="language-plaintext highlighter-rouge">A-Z</code> and <code class="language-plaintext highlighter-rouge">0-9</code>. We will also declare a fourth array combining all of these with any special characters.</p>
<p>To keep the code nice and concise, I’m going to make use of a couple of <a href="https://lodash.com/docs">lodash</a> methods.</p>
<p>Let’s start by installing that:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>npm <span class="nb">install </span>lodash
</code></pre></div></div>
<p>Then, to declare our character sets:</p>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">_</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="dl">'</span><span class="s1">lodash</span><span class="dl">'</span><span class="p">);</span>
<span class="p">...</span>
<span class="kd">const</span> <span class="nx">NUMBERS</span> <span class="o">=</span> <span class="nx">_</span><span class="p">.</span><span class="nx">range</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">10</span><span class="p">).</span><span class="nx">map</span><span class="p">(</span><span class="nx">num</span> <span class="o">=></span> <span class="nx">num</span><span class="p">.</span><span class="nx">toString</span><span class="p">());</span>
<span class="kd">const</span> <span class="nx">ALPHABET_LOWER</span> <span class="o">=</span> <span class="nx">_</span><span class="p">.</span><span class="nx">range</span><span class="p">(</span><span class="mi">97</span><span class="p">,</span> <span class="mi">123</span><span class="p">)</span>
<span class="p">.</span><span class="nx">map</span><span class="p">(</span><span class="nx">chr</span> <span class="o">=></span> <span class="nb">String</span><span class="p">.</span><span class="nx">fromCharCode</span><span class="p">(</span><span class="nx">chr</span><span class="p">));</span>
<span class="kd">const</span> <span class="nx">ALPHABET_UPPER</span> <span class="o">=</span> <span class="nx">_</span><span class="p">.</span><span class="nx">range</span><span class="p">(</span><span class="mi">65</span><span class="p">,</span> <span class="mi">91</span><span class="p">)</span>
<span class="p">.</span><span class="nx">map</span><span class="p">(</span><span class="nx">chr</span> <span class="o">=></span> <span class="nb">String</span><span class="p">.</span><span class="nx">fromCharCode</span><span class="p">(</span><span class="nx">chr</span><span class="p">));</span>
<span class="kd">const</span> <span class="nx">ALL_POSSIBLE_CHARS</span> <span class="o">=</span> <span class="nx">_</span><span class="p">.</span><span class="nx">range</span><span class="p">(</span><span class="mi">33</span><span class="p">,</span> <span class="mi">127</span><span class="p">)</span>
<span class="p">.</span><span class="nx">map</span><span class="p">(</span><span class="nx">chr</span> <span class="o">=></span> <span class="nb">String</span><span class="p">.</span><span class="nx">fromCharCode</span><span class="p">(</span><span class="nx">chr</span><span class="p">));</span>
</code></pre></div></div>
<p>Here, we’re making use of lodash’s <a href="https://lodash.com/docs#range">range</a> method, which creates an array of numbers from <code class="language-plaintext highlighter-rouge">start</code> up to, but not including, <code class="language-plaintext highlighter-rouge">end</code>. For everything other than our first array (0-9), we’re then mapping over these numbers to generate a second array of characters using the <a href="https://www.w3schools.com/charsets/ref_html_ascii.asp">ASCII Character Set</a>.</p>
<p>Next, we need to add a fifth array, containing two further arrays — one with all of the number and letter characters, and one containing every conceivable character we are going to use. Which one of these arrays is employed, depends upon whether the user has checked the <em>Use special characters</em> checkbox or not.</p>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">CHARSETS</span> <span class="o">=</span> <span class="p">[</span>
<span class="nx">ALL_POSSIBLE_CHARS</span><span class="p">,</span>
<span class="p">[...</span><span class="nx">NUMBERS</span><span class="p">,</span> <span class="p">...</span><span class="nx">ALPHABET_LOWER</span><span class="p">,</span> <span class="p">...</span><span class="nx">ALPHABET_UPPER</span><span class="p">]</span>
<span class="p">];</span>
</code></pre></div></div>
<p>If you want to check for yourself what these arrays contain, just try logging them to the console and restarting the app.</p>
<h2 id="adding-the-event-handler">Adding the Event Handler</h2>
<p>Now, let’s declare two functions: one to grab the relevant character set and one to generate the password</p>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Logic</span>
<span class="kd">function</span> <span class="nx">getCharSet</span><span class="p">(</span><span class="nx">includeSpecialCharacters</span><span class="p">)</span> <span class="p">{</span>
<span class="k">return</span> <span class="nx">includeSpecialCharacters</span><span class="p">?</span> <span class="nx">CHARSETS</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="p">:</span> <span class="nx">CHARSETS</span><span class="p">[</span><span class="mi">1</span><span class="p">];</span>
<span class="p">}</span>
<span class="kd">function</span> <span class="nx">generatePassword</span><span class="p">(</span><span class="nx">passwordLength</span><span class="p">,</span> <span class="nx">charSet</span><span class="p">)</span> <span class="p">{</span>
<span class="k">return</span> <span class="nx">_</span><span class="p">.</span><span class="nx">range</span><span class="p">(</span><span class="nx">passwordLength</span><span class="p">).</span><span class="nx">map</span><span class="p">(()</span> <span class="o">=></span> <span class="nx">_</span><span class="p">.</span><span class="nx">sample</span><span class="p">(</span><span class="nx">charSet</span><span class="p">)).</span><span class="nx">join</span><span class="p">(</span><span class="dl">''</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>
<p>And hook up the event listener to take advantage of them:</p>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Event handling</span>
<span class="nx">generateButton</span><span class="p">.</span><span class="nx">addEventListener</span><span class="p">(</span><span class="nx">QPushButtonEvents</span><span class="p">.</span><span class="nx">clicked</span><span class="p">,</span> <span class="p">()</span> <span class="o">=></span> <span class="p">{</span>
<span class="kd">const</span> <span class="nx">passwordLength</span> <span class="o">=</span> <span class="nx">numCharsInput</span><span class="p">.</span><span class="nx">text</span><span class="p">();</span>
<span class="kd">const</span> <span class="nx">includeSpecialChars</span> <span class="o">=</span> <span class="nx">checkbox</span><span class="p">.</span><span class="nx">isChecked</span><span class="p">();</span>
<span class="kd">const</span> <span class="nx">charSet</span> <span class="o">=</span> <span class="nx">getCharSet</span><span class="p">(</span><span class="nx">includeSpecialChars</span><span class="p">);</span>
<span class="nx">passOutput</span><span class="p">.</span><span class="nx">setPlainText</span><span class="p">(</span>
<span class="nx">generatePassword</span><span class="p">(</span><span class="nx">passwordLength</span><span class="p">,</span> <span class="nx">charSet</span><span class="p">)</span>
<span class="p">);</span>
<span class="p">});</span>
</code></pre></div></div>
<p>As you can see, when the user clicks the <em>Generate</em> button, we determine the password length, whether special characters are required, and which character set to use.</p>
<p>Then we call the <code class="language-plaintext highlighter-rouge">generatePassword</code> function which creates an array of numbers corresponding to the desired password length. It then maps over this array and uses lodash’s <a href="https://lodash.com/docs#sample">sample</a> method to grab random characters from whatever character set it was passed. It then calls <code class="language-plaintext highlighter-rouge">join()</code> on this array to turn it into a string, which it returns.</p>
<p>Finally, this return value is set as the value of the <code class="language-plaintext highlighter-rouge">QPlainText</code> widget.</p>
<p>At this point we have a functioning password generator.</p>
<p><img src="https://res.cloudinary.com/hibbard/image/upload/v1569338593/node-gui/node-gui-08.png" alt="App generating a 66 digit password" /></p>
<h2 id="adding-copy-to-clipboard">Adding Copy to Clipboard</h2>
<p>To round things off, let’s implement the copy to clipboard functionality. This can be done in a couple of lines of code using the <a href="https://docs.nodegui.org/docs/api/generated/classes/qapplication">QApplication</a> module, which manages the application’s control flow and main settings.</p>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="p">{</span>
<span class="nx">QApplication</span><span class="p">,</span>
<span class="p">...</span>
<span class="p">}</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="dl">'</span><span class="s1">@nodegui/nodegui</span><span class="dl">'</span><span class="p">);</span>
<span class="c1">// Clipboard</span>
<span class="kd">const</span> <span class="nx">clipboard</span> <span class="o">=</span> <span class="nx">QApplication</span><span class="p">.</span><span class="nx">clipboard</span><span class="p">();</span>
<span class="c1">// Event handling</span>
<span class="p">...</span>
<span class="nx">copyButton</span><span class="p">.</span><span class="nx">addEventListener</span><span class="p">(</span><span class="nx">QPushButtonEvents</span><span class="p">.</span><span class="nx">clicked</span><span class="p">,</span> <span class="p">()</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">clipboard</span><span class="p">.</span><span class="nx">setText</span><span class="p">(</span><span class="nx">passOutput</span><span class="p">.</span><span class="nx">toPlainText</span><span class="p">(),</span> <span class="mi">0</span><span class="p">);</span>
<span class="p">});</span>
</code></pre></div></div>
<p>Here <code class="language-plaintext highlighter-rouge">QApplication.clipboard()</code> returns an object for interacting with the clipboard. We can use this object’s <code class="language-plaintext highlighter-rouge">setText</code> method to alter the actual clipboard’s contents.</p>
<p>Run the app and give it a try to satisfy yourself that it works.</p>
<h2 id="distribution">Distribution</h2>
<p>Before calling it a day, let’s look at how to package and distribute our app. To do this, we’ll need to install an extra dependency called <a href="https://github.com/nodegui/packer">Packer</a>.</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>npm <span class="nb">install</span> <span class="nt">--save-dev</span> @nodegui/packer
</code></pre></div></div>
<p>Once it has downloaded and installed you need to run the <code class="language-plaintext highlighter-rouge">init</code> command from the project root.</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>npx nodegui-packer <span class="nt">--init</span> PasswordGenerator
</code></pre></div></div>
<p>This will create a <code class="language-plaintext highlighter-rouge">deploy</code> directory containing a template. You can modify this template to suit your needs, for example by adding icons, changing the name/description, or adding other native features.</p>
<p>Finally run the <code class="language-plaintext highlighter-rouge">pack</code> command.</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>npx nodegui-packer --pack dist
</code></pre></div></div>
<p>This command creates a <code class="language-plaintext highlighter-rouge">build</code> directory inside of the <code class="language-plaintext highlighter-rouge">deploy</code> directory containing a platform-specific standalone executable.</p>
<h2 id="i-thought-you-said-cross-platform">I thought You Said Cross-platform?!</h2>
<p>Yeah, sorry, I did.</p>
<p>NodeGUI is a young project and unfortunately cross-platform support is not quite there yet. That is to say, if you need cross-platform builds, you have to run the packer in each of the different OS environments.</p>
<p>Also, while we’re at it, I called Electron bloated at the top of the article. However, examining the <code class="language-plaintext highlighter-rouge">AppImage</code> file produced on Linux, one sees that it’s 45MB in size! That’s not exactly slimline…</p>
<p>Both of these points are on the <a href="https://github.com/nodegui/packer#future-enhancements">Packer roadmap</a> to be addressed in a future release. Nonetheless they bear mentioning here.</p>
<h2 id="getting-help-and-contributing">Getting Help and Contributing</h2>
<p>While building the password generator app, there were a couple of times that I ran up against problems. For example, I was unsure how to get a <code class="language-plaintext highlighter-rouge">QCheckBox</code>’s checked state, so I opened an <a href="https://github.com/nodegui/nodegui/issues/66">issue on the project’s homepage</a>. As you can see, I got an answer a short while later from the project maintainer, who is both very friendly and very helpful.</p>
<p>If you run into any issues while using NodeGUI, the issues section is a good place to ask for help.</p>
<h3 id="contributing">Contributing</h3>
<p>The NodeGUI project is actively looking for contributors and there are several ways you can help. You can find the <a href="https://github.com/nodegui/nodegui/tree/master/website/docs/development">contributor’s guide</a> here.</p>
<p>If you have a basic C++ knowledge, you can help by adding unexported methods to existing widgets — <a href="https://github.com/nodegui/nodegui/issues/36">this issue</a> and <a href="https://github.com/nodegui/nodegui/pull/39">this PR</a> can be used as a guide</p>
<p>You can also help by addressing bugs, or contributing to the documentation. <a href="https://hacktoberfest.digitalocean.com/">Hacktoberfest</a> starts soon and it’d be awesome to see this project get some love.</p>
<h2 id="conclusion">Conclusion</h2>
<p>In this article I have demonstrated how to get up and running with NodeGUI. I introduced you to several of the library’s basic concepts and have shown how to build and package a simple app.</p>
<p>As a next step, I would encourage you to build something cool of your own.</p>
<p>And please contribute back to the project if you can. It would be amazing if NodeGUI gained some traction.</p>James HibbardNodeGUI is an open source library for building cross-platform, native desktop applications with JavaScript and CSS-like styling. In this article, I’m going to demonstrate how to get up and running with NodeGUI. We’ll set up a development environment, take a look at several of the library’s basic concepts, then finish off by creating a simple password generator app. If you’re curious as to what we’ll end up with, the finished code can be found on GitHub.A Beginner’s Guide to Installing a Custom ROM on an Android Phone2019-03-08T00:00:00+00:002019-03-08T00:00:00+00:00https://hibbard.eu/flash-android-custom-rom<p>I have an old Sony XPeria S phone (from 2012) which has long since been abandoned by my carrier. It no longer receives any updates and is stuck on Android 4.2, which kinda sucks….</p>
<p>So I decided to take matters into my own hands and get an up-to-date version of Android, by flashing the phone with a custom ROM. While this didn’t prove to be that difficult, I did hit a few stumbling blocks along the way, which I wanted to document.</p>
<p>This post will serve two purposes. Firstly, it will provide detailed instructions on how to install a custom ROM on an Xperia S. Secondly, it will also outline the general process of flashing an old Android phone and offer a high-level overview of the concepts involved.</p>
<!--more-->
<h2 id="but-why">But Why?</h2>
<p>The big motivational factors for me wanting to do this were:</p>
<ul>
<li><a href="https://www.fastcompany.com/90165365/smartphones-are-wrecking-the-planet-faster-than-anyone-expected">The environmental impact of producing new smartphones is not negligible</a>. Why should I be forced to buy a new phone every two years when an old one will serve me just as well?</li>
<li><a href="http://www.xperiablog.net/2016/09/14/sony-needs-to-work-harder-on-bloatware/">New smartphones come loaded with bloatware which cannot be uninstalled</a>. This ranges from the manufacturer’s own software, to 3rd party apps (e.g. Amazon, Facebook) which come bundled with the phone. Removing these apps can increase battery life and reduce your attack surface.</li>
<li><a href="https://www.theguardian.com/commentisfree/2018/mar/28/all-the-data-facebook-google-has-on-you-privacy">The amount of data Google collects about its users is frightening</a>. By installing a custom ROM, you can massively reduce Google’s data collection on your mobile device.</li>
<li><a href="https://www.cbronline.com/news/cybersecurity/android-users-warned-security-risk-versions-older-oreo/">Running an unsupported Android version is a security risk</a>. If you chose a custom ROM that is under active development, your old phone will still receive security updates.</li>
</ul>
<h2 id="and-why-not">And Why Not?</h2>
<p>It’s also important to consider why this might not be the best idea.</p>
<ul>
<li>To install a custom ROM, you will have to unlock your phone’s bootloader. <strong>This could void your phone’s warranty</strong>.</li>
<li>Unlocking the bootloader will perform a factory reset of your phone. This means <strong>you will lose any data which is not backed up</strong>.</li>
<li>You have no guarantee of success. If the process goes wrong, <strong>it is possible to make the phone unusable</strong>.</li>
<li><strong>Security</strong>. Looking at this from a different angle, you will ultimately be downloading your phone’s new operating system from a file sharing site somewhere on the internet. Take a second to think about that.</li>
</ul>
<p>For anyone who is unsure what they are doing, please read the tutorial through to the end. Do <em>not</em> start issuing random commands to your device without understanding what they do. I am in <em>no way</em> responsible if you brick your phone.</p>
<p>And with that said, let’s get to it…</p>
<h2 id="the-general-principles">The General Principles</h2>
<p>For this process to work, you’ll need to do the following:</p>
<ul>
<li>Find a custom ROM to install</li>
<li>Install the Android SDK Platform-Tools</li>
<li>Unlock the device’s bootloader</li>
<li>Install a custom recovery image</li>
<li>Use the recovery image to install the ROM</li>
</ul>
<p>You’ll also need to connect your device to your PC, so have a suitable USB cable handy. And please note that I am using Linux. The process will differ slightly if you are using Windows, but the principles are similar.</p>
<h2 id="so-what-is-a-rom">So What Is a ROM?</h2>
<p>ROM stands for <strong>R</strong>ead-<strong>O</strong>nly <strong>M</strong>emory — a storage medium whose data may only be read. An example of a ROM would be a cartridge used in a video game console.</p>
<p>In terms of smartphones, the phrase has taken on a slightly different meaning, namely: an image of an operating system that you install into the ROM area of your phone.</p>
<p>There are two common kinds of ROM. A <strong>stock ROM</strong> is the version of the operating system which comes with the phone, whereas a <strong>custom ROM</strong> is a version of the operating system which has been modified in some way.</p>
<p>It is possible to mod Android, because <a href="https://www.android.com/">Android is open source</a>. This means that any developer can edit the code, recompile it, and re-release it for people to install.</p>
<h2 id="finding-a-custom-rom">Finding a Custom ROM</h2>
<p>Custom ROMs are device specific. This means you have to find one which has been tailored to your device. You <em>cannot</em> just grab the first one that takes your fancy.</p>
<p>A great place to visit when looking for a custom ROM (or anything else Android related) is the <a href="https://forum.xda-developers.com/">XDA-Developers Forum</a>. This is a site where Android developers and users hang out to exchange ideas, apps, resources etc.</p>
<p>When you first arrive, you can enter your device into the device finder at the top of the page. I was happy to see that <a href="https://forum.xda-developers.com/xperia-s">the Xperia S has its own forum</a> where I soon discovered the <a href="https://forum.xda-developers.com/xperia-s/s-development/rom-naosprom-xperia-s-t3853082">nAOSP 8.1 Oreo ROM</a> on offer. Note that “ASOP” stands for <strong>A</strong>ndroid <strong>O</strong>pen <strong>S</strong>ource <strong>P</strong>roject (the open-source version of Android I mentioned above) and that the “n” stands for “near”. 8.1 is the Android version number and “Oreo” is the version name.</p>
<p>Cool! So I was good to go with a ROM. This would be a significant leap from the crappy 4.2 version I had been stuck on.</p>
<h2 id="unlocking-the-bootloader">Unlocking the Bootloader</h2>
<p>On a PC, a bootloader is a program that starts whenever a device is powered on, so as to ensure the right operating system is activated. For example, maybe you installed Windows and Linux on the same machine and the PC needs to know which one to boot into.</p>
<p>The same principle applies to an Android phone, except the bootloader determines when to run Android and when to enter recovery mode. You need to unlock the bootloader, so as to be able to install a custom recovery image and a custom ROM.</p>
<p>Returning to the Xperia, a quick Google search brought me to <a href="https://developer.sony.com/develop/open-devices/get-started/unlock-bootloader/how-to-unlock-bootloader/">the official instructions on the Sony site</a>. I’ll go through the steps I followed below.</p>
<h3 id="check-if-the-bootloader-can-be-unlocked">Check If the Bootloader Can Be Unlocked</h3>
<p>As it is only possible to unlock the bootloader for certain releases, I needed to verify that the bootloader on my device could be unlocked.</p>
<p>To do this, I had to open the dialer and enter <code class="language-plaintext highlighter-rouge">*#*#7378423#*#*</code> to access the service menu.</p>
<p>Then, I had to tap <em>Service info</em> > <em>Configuration</em> > <em>Rooting Status</em>.</p>
<p>Happily, this said “Yes”</p>
<h3 id="install-android-sdk-platform-tools">Install Android SDK Platform-Tools</h3>
<p>As can be read on <a href="https://developer.android.com/studio/releases/platform-tools.html">the project’s website</a>, Android SDK Platform-Tools is a component for the Android SDK. It includes tools that interface with the Android platform, such as <code class="language-plaintext highlighter-rouge">adb</code> and <code class="language-plaintext highlighter-rouge">fastboot</code>. These tools are required if you want to unlock your device’s bootloader and flash it with a new system image.</p>
<p>I downloaded the correct version of the tools (<a href="https://dl.google.com/android/repository/platform-tools-latest-linux.zip">SDK Platform-Tools for Linux</a>) from the page above and unpacked it to my home directory. I then entered the <code class="language-plaintext highlighter-rouge">platform-tools</code> directory and added the <code class="language-plaintext highlighter-rouge">adb</code> and <code class="language-plaintext highlighter-rouge">fastboot</code> programs to my <code class="language-plaintext highlighter-rouge">$PATH</code>.</p>
<p>Finally, I needed to enable USB debugging on my Xperia. This could be done by tapping on <em>Settings</em> > <em>About Phone</em> > <em>Build Version</em> multiple times, after which I could turn it on via <em>Settings</em> > <em>Developer options</em>.</p>
<h3 id="connect-to-fastboot">Connect to Fastboot</h3>
<p>Fastboot is a tool/protocol for writing data directly to your phone’s flash memory. To boot the phone into fastboot mode, I needed to connect it to the PC via USB, then issue the following command:</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>adb reboot bootloader
</code></pre></div></div>
<p>The light in the top left-hand corner of the device changed from green to blue to indicate that the device was in fastboot mode.</p>
<p>Next I entered:</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>fastboot devices
</code></pre></div></div>
<p>And confirmed that my device was listed here by its serial number.</p>
<h3 id="obtain-an-unlock-key">Obtain an Unlock Key</h3>
<blockquote>
<p>This step will void your warranty and delete your data! If this worries you, check out <a href="https://www.droidviews.com/backup-ta-partition-xperia-devices/">this post on making a TA backup</a>, which should let you tamper with your device, without worrying about the warranty.</p>
</blockquote>
<p>To complete the final step I needed my phone’s IMEI number. IMEI stands for <strong>I</strong>nternational <strong>M</strong>obile <strong>S</strong>tation <strong>E</strong>quipment <strong>I</strong>dentity and is a unique set of numbers and letters that identifies the device.</p>
<p>To obtain the IMEI number, I needed to dial <code class="language-plaintext highlighter-rouge">*#06#</code>, then make a note of what was displayed.</p>
<p>Next, I had to head over to the Sony’s <a href="https://developer.sony.com/develop/open-devices/get-started/unlock-bootloader/">Unlock Bootloader page</a>, select my device and enter the IMEI number I had just jotted down. Having done that, the website returned my unlock code.</p>
<p>Finally, back in the terminal, I had to enter:</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>fastboot oem unlock 0x<unlock code>
</code></pre></div></div>
<p>At this point, the phone disconnected and took a while to boot back up. When it did, it was completely wiped and started setting Android up for the first use.</p>
<p>And the bootloader was unlocked.</p>
<h2 id="installing-a-custom-recovery">Installing a Custom Recovery</h2>
<p>The thread in which I found the ROM links to <a href="https://forum.xda-developers.com/showpost.php?p=78410803&postcount=58">additional instructions for installing it</a>. Chief among them is:</p>
<blockquote>
<p>This build must be flashed from the latest version of TWRP (3.2.3).</p>
</blockquote>
<p>TWRP is short for <a href="https://twrp.me/">Team Win Recovery Project</a> and is an open-source custom recovery image for Android-based devices. TWRP is a powerful tool that allows users to install third-party firmware (i.e. custom ROMs) and back up the current system. These features are normally unsupported by stock recovery images.</p>
<p>As with custom ROMs, the version of TWRP that you install will depend upon the phone you are installing it onto. In my case, I chose the latest version of <a href="https://twrp.me/sony/sonyxperias.html">TWRP for Sony Xperia S</a>. If you are following along here, please make sure that you select <a href="https://twrp.me/Devices/">the correct version of TWRP for your device</a>.</p>
<h3 id="installation-woes">Installation Woes</h3>
<p>Unfortunately for me, this was the trickiest step in the process and it cost me a fair deal of time. Originally, I had been following <a href="https://www.xda-developers.com/how-to-install-twrp/">this guide on the XDA-Developers forums</a>, which states that once your phone is connected to your PC in fastboot, you can flash your phone’s recovery with the following command:</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>fastboot flash recovery twrp-3.2.3-nozomi.img
</code></pre></div></div>
<p>I ran this command, but annoyingly it errored out with the following message:</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Sending <span class="s1">'recovery'</span> <span class="o">(</span>12964 KB<span class="o">)</span>
<span class="o">(</span>bootloader<span class="o">)</span> USB download speed was 33272kB/s
OKAY <span class="o">[</span> 0.415s]
Writing <span class="s1">'recovery'</span>
<span class="o">(</span>bootloader<span class="o">)</span> Flash of partition <span class="s1">'recovery'</span> requested
FAILED <span class="o">(</span>remote: <span class="s1">'Partition not found'</span><span class="o">)</span>
Finished. Total <span class="nb">time</span>: 0.437s
</code></pre></div></div>
<p>It seems that the reason for this is that Sony Xperia S phones, don’t have a dedicated recovery partition, rather a recovery-in-boot arrangement, which means that the recovery is booted using the regular kernel/boot image on the device.</p>
<p>The next method I tried was listed on the TWRP for Sony Xperia S page linked to above (it might have been a good idea to read that first time around). This saw me rename the TWRP image to <code class="language-plaintext highlighter-rouge">twrp.img</code>, then copy it to the root of the phone’s <code class="language-plaintext highlighter-rouge">/sdcard</code> folder like so:</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">mv </span>twrp-3.2.3-nozomi.img twrp.img
adb push twrp.img /sdcard
</code></pre></div></div>
<p>Then open an adb shell and use <code class="language-plaintext highlighter-rouge">dd</code> to copy it to the correct location on the device:</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>adb shell
shell@android:/ <span class="nv">$ </span>su
root@android:/ <span class="c"># dd if=/sdcard/twrp.img of=/dev/block/mmcblk0p11</span>
25929+1 records <span class="k">in
</span>25929+1 records out
</code></pre></div></div>
<p>This seemed to work, i.e. I didn’t get an error message, but try as I liked, I could <em>not</em> get the phone to boot into the custom recovery. Meh!</p>
<h3 id="what-else-didnt-work">What Else Didn’t Work</h3>
<p>At this point I started Googling, but to no avail. There are a heap of tutorials out there on how to flash the Xperia S with a custom ROM (<a href="https://www.youtube.com/watch?v=h6IU2-QcVD8">here</a> <a href="https://www.youtube.com/watch?v=FVRahxHoSdE">are</a> <a href="https://theunlockr.com/how-to-install-twrp-recovery-on-the-sony-xperia-s/">some</a> <a href="http://www.gadgetsacademy.com/install-twrp-recovery-xperia-s-lt26i-nozomi/">examples</a>), but they are either outdated, assume TWRP is already installed, or tell you to use one of the methods listed above that didn’t work.</p>
<p>I then attempted to use the official <a href="https://play.google.com/store/apps/details?id=me.twrp.twrpapp&hl=en">TWRP App</a> to install TWRP from within Android. I followed the instructions in <a href="https://www.youtube.com/watch?v=wEHChHcpZEI">this video</a>, but when it came to selecting the downloaded image to flash, I didn’t have the option. The app also reported <code class="language-plaintext highlighter-rouge">Can't find recovery</code> — indicating the previous error was causing it to fail.</p>
<blockquote>
<p>The TWRP app requires you to root your phone. To do this I headed over to the <a href="https://towelroot.com/">TowelRoot site</a>, where I clicked the big red lambda (λ) to download <code class="language-plaintext highlighter-rouge">tr.apk</code>, then ran <code class="language-plaintext highlighter-rouge">adb install tr.apk</code> to install the app. I turned on the phone’s Wi-Fi (apparently, the app needs it to check if the phone is rootable), opened the app on the phone and that was it.</p>
</blockquote>
<p>Next I tried <a href="https://play.google.com/store/apps/details?id=com.jmz.soft.twrpmanager&hl=en">TWRP Manager</a> which does more or less the same thing as the official TWRP app above. During installation, this pulled in something called <a href="https://www.busybox.net/">Busybox</a> (which was good to know about). I followed <a href="https://www.youtube.com/watch?time_continue=59&v=JhRcANciGYI">this video</a> to install TWRP from within Android, but again (frustratingly), it failed when it came to the download. So, no dice..</p>
<p>I then saw ROM Manager recommended as <a href="https://alternativeto.net/software/team-win-recovery-project-twrp-/">an alternative to TWRP</a>. The official website is <a href="http://clockworkmod.com/">http://clockworkmod.com/</a> and you can click through to <a href="https://play.google.com/store/apps/details?id=com.koushikdutta.rommanager&hl=en">ROM manager in the Play Store</a> from there. I watched the official video (on that page), installed ROM manager, but when I went to install the Clockwork Mod recovery from within the app, it told me that my phone wasn’t supported.</p>
<p>Finally, I tried to install the Clock Work Mod (CWM) recovery directly. <a href="https://forum.xda-developers.com/showthread.php?t=2302393">This thread on XDA-Developers forum</a> looked promising, but all the links were dead. I also tried following <a href="https://www.youtube.com/watch?v=BiEzkZG1CIM">this tutorial</a>, but it relied on the missing file from the thread above.</p>
<h2 id="what-did-work">What Did Work</h2>
<p>By this point, I had a sinking feeling that everything I tried was doomed to failure. Then, I ran across <a href="https://wiki.lineageos.org/devices/huashan/install">a LineageOS page</a> that said:</p>
<blockquote>
<p>Temporarily flash TWRP to boot: <code class="language-plaintext highlighter-rouge">fastboot flash boot twrp-x.x.x-x-huashan.img</code></p>
</blockquote>
<p>I had read that TWRP should not be flashed to boot, but by now I was out of options with nothing to really lose. So I did:</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>fastboot flash boot twrp-3.2.3-nozomi.img
</code></pre></div></div>
<p>This worked (in that the command ran without errors) and I tentatively restarted the phone. Once it booted back up again, the first thing I noticed was that the light in the top left corner turned pink. It hadn’t done that before, so I pressed <em>Volume up</em> a couple of times, and I was in to the recovery. Yay!</p>
<p>Eager to test the next step, I booted back into fastboot and moved the custom ROM to the phone:</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>fastboot reboot
adb push nAOSProm-8.1.0-b06-nozomi.zip /sdcard
</code></pre></div></div>
<p>Then, I could jump back into TWRP (as described above) and select <em>Install</em> > <em>nAOSProm-8.1.0-b06-nozomi.zip</em> > <em>Swipe to Confirm Flash</em> to install the custom ROM.</p>
<p><img src="https://res.cloudinary.com/hibbard/image/upload/v1552309037/twrp.png" alt="Swipe to confirm flash" /></p>
<p>A progress bar showed on TWRP for a while, then the phone rebooted. It took ages to come back to life, but when it did, it was up and running with Android Oreo 8.1!</p>
<h2 id="installing-open-source-apps-with-f-droid">Installing Open-Source Apps with F-Droid</h2>
<p>The ROM came with only a handful of apps installed, so one of the first things I did with the new OS was to install <a href="https://f-droid.org/">F-Droid</a>. This is a community-maintained software repository for Android, similar to the Google Play store. The main repository, hosted by the project, contains only free, libre software apps.</p>
<p>Installing F-Droid is simple. You can <a href="https://f-droid.org/en/packages/org.fdroid.fdroid/">download it from the official F-Droid website</a>. Then, locate the downloaded apk file on your device and tap it to install. You will be prompted to allow Android to install an app form an unknown source. Agree to this and you’re good to go.</p>
<p>Once you open F-Droid, you will see that the apps are organized into categories, such as <em>Connectivity</em>, <em>Development</em>, <em>Games</em> and so on. There is a reasonably good selection of apps out of the box, but you can expand this by installing additional repos, many of which are <a href="https://forum.f-droid.org/t/known-repositories/721">listed here</a>. You can read <a href="https://www.f-droid.org/en/tutorials/add-repo/">how to add a repo to F-Droid here</a>.</p>
<p>If you’re looking for inspiration on what top install, check out <a href="https://lushka.al/my-android-setup/#next-part-installing-the-apps-and-configuring-everything">this post</a> where the author lists his favourite F-Droid apps.</p>
<h2 id="but-i-cant-live-without-insert-app-here">But I Can’t Live Without <code class="language-plaintext highlighter-rouge"><insert-app-here></code></h2>
<p>Personally speaking, I’m happy to keep Google off of my phone as much as possible, but if you really need apps out of the Play Store, or any of the proprietary Google-branded applications that come pre-installed with most Android devices, there are a couple of possibilities.</p>
<p>The most lightweight option is an app called <a href="https://f-droid.org/en/packages/com.dragons.aurora/">Aurora Store</a> which can be downloaded via F-Droid. This lets you download apps directly from Google Play Store as apk files which you can then install.</p>
<p>The second option is to install something called <a href="https://opengapps.org/">Open GApps</a>. This will allow you to add part, or all of Google’s suite of apps (such as Google Play Services, Play Store, Gmail, Maps, etc) back to your phone. You can hover your mouse over the different packages on the project’s home page to see what each variant offers. You will also have to choose the correct platform and Android version before you can download. If you’re not sure what to select, <a href="https://opengapps.org/app/">the project has its own app</a>, which will detect which version of Android you’re running and which architecture the apps need to be built for.</p>
<h3 id="open-gapps-on-the-xperia-s">Open GApps on the Xperia S</h3>
<p>I was curious to see if I could get Google Play working on the Xperia S and this is how I went.</p>
<p>In <a href="https://forum.xda-developers.com/xperia-s/s-development/rom-naosprom-xperia-s-t3853082">the original thread on the XDA-Developers forum</a>, the ROM dev was saying that <a href="https://forum.xda-developers.com/showpost.php?s=143b154fa10019aa754c18f5c3812bcf&p=77859191&postcount=10">Open GApps Arm 8.1 Micro was a good choice</a>, so I downloaded this and pushed it to the phone:</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>adb push open_gapps-arm-8.1-micro-20190225.zip /sdcard
</code></pre></div></div>
<p>I then hopped into TWRP and installed it using the same method as outlined above. When I restarted the phone, the apps were there, but when I started Google Play, I kept repeatedly getting the message “Play service stopped working”.</p>
<p>I then tried to uninstall GApps using <a href="https://github.com/CHEF-KOCH/Remove-Gapps">this tool</a> and rendered the OS unusable. Oh dear!</p>
<p>Going back to square one, I then wiped the device from within TWRP (advanced wipe including the system). I then pushed the custom ROM to the device, downloaded GApps <em>Arm</em> > <em>8.1</em> > <em>Pico</em> and pushed that to the device, too. Pico contains the bare minimum to get Google Play functionality.</p>
<p>From within TWRP, I clicked <em>Install</em>, then selected first the custom ROM, then the GApps file. TWRP installed them one after another and rebooted the phone.</p>
<p>The phone took a good 5 minutes to reboot, but when it did, Google Play was there! I logged into the Play store and installed the <a href="https://play.google.com/store/apps/details?id=org.eu.exodus_privacy.exodusprivacy&hl=en_US">Exodus Privacy</a> app to test it was working. It was. Yay!</p>
<h2 id="conclusion">Conclusion</h2>
<p>I couldn’t be happier with how this has all worked out. I’ve resurrected an old device to run an up-to-date version of Android and in the process have reduced Google’s ability to track me more than it already does. Here’s what the new OS looks like:</p>
<p><img src="https://res.cloudinary.com/hibbard/image/upload/v1552313907/orio-8.1.png" alt="Orio 8.1 Home Screen, Apps and Settings" /></p>
<p>This post has been a bit rambling and in places more of a “note to future self”. Nonetheless I hope it has helps anyone looking to get started with flashing their Android with a custom ROM. I’d be happy to hear what you though in the comments below.</p>James HibbardI have an old Sony XPeria S phone (from 2012) which has long since been abandoned by my carrier. It no longer receives any updates and is stuck on Android 4.2, which kinda sucks…. So I decided to take matters into my own hands and get an up-to-date version of Android, by flashing the phone with a custom ROM. While this didn’t prove to be that difficult, I did hit a few stumbling blocks along the way, which I wanted to document. This post will serve two purposes. Firstly, it will provide detailed instructions on how to install a custom ROM on an Xperia S. Secondly, it will also outline the general process of flashing an old Android phone and offer a high-level overview of the concepts involved.How to Create a Simple CRUD App with Rails and React (Class-based Version)2019-01-26T00:00:00+00:002019-01-26T00:00:00+00:00https://hibbard.eu/crud-app-rails-react-classes<div class="outdated-content">
<p>
<img src="https://res.cloudinary.com/hibbard/image/upload/v1646913760/icons/exclamation-mark.png" />
This content is outdated and has been left here for reference purposes.
If you are building a CRUD app with React today, you should probably follow
<a href="https://hibbard.eu/rails-react-crud-app/">this updated tutorial</a> instead.
</p>
</div>
<p>Most web applications need to persist data in one form or other. When working with a server-side language, this is normally a straightforward task. However when you add a front-end JavaScript framework to the mix, things start to get a bit trickier.</p>
<p>In this tutorial I am going to demonstrate how to build a JSON API using Ruby on Rails and then code a fully-functional React frontend to interact with the API. The app we’ll be building is an event manager, which will let you create and manage a list of academic events.</p>
<p>The app will showcase basic CRUD functionality and will add a couple of extra features (such as a datepicker and search). To integrate the React frontend with the Rails backend, I’ll be using the Webpacker gem, which will ship as the default JavaScript bundler for Rails 6.</p>
<p>This is what the finished app will look like.</p>
<p><img src="https://res.cloudinary.com/hibbard/image/upload/v1549014887/event-manager/event-manager-06.png" alt="Event Manager - Flash message" />
You can find the <a href="https://github.com/jameshibbard/react-rails-crud-app/tree/classes">complete code for the tutorial on GitHub</a>.</p>
<h2 id="prerequisites">Prerequisites</h2>
<p>To follow along, you’ll need both Ruby and Node installed on your system. For Ruby you can either go <a href="https://www.ruby-lang.org/en/downloads/">here</a> and download the official binaries for your system, or use a version manager such as <a href="https://github.com/rbenv/rbenv">rbenv</a>.</p>
<p>The same goes for Node. You can either go <a href="https://nodejs.org/en/">here</a> and download the official binaries for your system, or use a version manager such as <a href="https://github.com/creationix/nvm">nvm</a>.</p>
<p>In both cases I would encourage people to use a version manager. They are easy to set up and make managing multiple versions of Node/Ruby a breeze. They also help negate permissions problems, meaning you don’t end up having to install gems/packages with admin rights.</p>
<p>For this tutorial I’ll be using Ruby version 2.6, Rails version 5.2.2. and Node version 10.15.0 (the latest LTS). My operating system is Linux Mint, so any terminal related commands will be tailored towards ‘nix.</p>
<h2 id="building-the-api">Building the API</h2>
<p>The first thing we’ll need to do is to install <a href="https://rubyonrails.org/">Rails</a> and create a new Rails app.</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>gem <span class="nb">install </span>rails
rails new event-manager
</code></pre></div></div>
<p>Depending on your OS, you might need to install the libsqlite3-dev library, as unless otherwise specified, Rails uses SQLite as its database.</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>apt-get <span class="nb">install </span>libsqlite3-dev
</code></pre></div></div>
<h3 id="model">Model</h3>
<p>Next, change into the project directory and create an <code class="language-plaintext highlighter-rouge">Event</code> model:</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>rails g model Event <span class="se">\</span>
event_type:string <span class="se">\</span>
event_date:date <span class="se">\</span>
title:text <span class="se">\</span>
speaker:string <span class="se">\</span>
host:string <span class="se">\</span>
published:boolean
</code></pre></div></div>
<p>Create the databases and tables:</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>rake db:create
rake db:migrate
</code></pre></div></div>
<p>Finally, seed the model with some test data. You can do this by creating a <code class="language-plaintext highlighter-rouge">db/seeds/events.json</code> file and adding the contents from the <a href="https://github.com/jameshibbard/react-rails-crud-app/blob/classes/db/seeds/events.json">corresponding file in the project repo</a>.</p>
<p>Then in <code class="language-plaintext highlighter-rouge">db/seeds.rb</code>, add:</p>
<div class="language-rb highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">json</span> <span class="o">=</span> <span class="no">ActiveSupport</span><span class="o">::</span><span class="no">JSON</span><span class="p">.</span><span class="nf">decode</span><span class="p">(</span><span class="no">File</span><span class="p">.</span><span class="nf">read</span><span class="p">(</span><span class="s1">'db/seeds/events.json'</span><span class="p">))</span>
<span class="n">json</span><span class="p">.</span><span class="nf">each</span> <span class="k">do</span> <span class="o">|</span><span class="n">record</span><span class="o">|</span>
<span class="no">Event</span><span class="p">.</span><span class="nf">create!</span><span class="p">(</span><span class="n">record</span><span class="p">)</span>
<span class="k">end</span>
</code></pre></div></div>
<p>And run <code class="language-plaintext highlighter-rouge">rake db:seed</code>. Start up the rails console with <code class="language-plaintext highlighter-rouge">rails c</code> and confirm that you have some data:</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>rails c
Running via Spring preloader <span class="k">in </span>process 32213
Loading development environment <span class="o">(</span>Rails 5.2.2<span class="o">)</span>
irb<span class="o">(</span>main<span class="o">)</span>:001:0> Event.all.count
<span class="o">(</span>0.3ms<span class="o">)</span> SELECT COUNT<span class="o">(</span><span class="k">*</span><span class="o">)</span> FROM <span class="s2">"events"</span>
<span class="o">=></span> 6
</code></pre></div></div>
<h3 id="controllers">Controllers</h3>
<p>In the next step, we’ll create an <code class="language-plaintext highlighter-rouge">Events</code> controller to respond to incoming requests to our API. We’ll put the controller in its own folder, as we’re going to namespace it. This will keep our code nice and organized and allow us to create our own set of routes for the API.</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">mkdir </span>app/controllers/api
<span class="nb">touch </span>app/controllers/api/events_controller.rb
</code></pre></div></div>
<p>Next, we’re going to install the <a href="https://github.com/plataformatec/responders">responders gem</a>. This will provide us with a <code class="language-plaintext highlighter-rouge">respond_with</code> method which will keep the controller code nice and DRY. This method used to be part of Rails core, but was moved into a gem in Rails 4.2.</p>
<p>Add the following to your <code class="language-plaintext highlighter-rouge">Gemfile</code>:</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>gem <span class="s1">'responders'</span>
</code></pre></div></div>
<p>Then run the <code class="language-plaintext highlighter-rouge">bundle</code> command.</p>
<p>Next, add the following code to <code class="language-plaintext highlighter-rouge">app/controllers/api/events_controller.rb</code>. Notice that we’ve namespaced it under <code class="language-plaintext highlighter-rouge">Api</code>.</p>
<div class="language-rb highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">Api::EventsController</span> <span class="o"><</span> <span class="no">ApplicationController</span>
<span class="n">respond_to</span> <span class="ss">:json</span>
<span class="k">def</span> <span class="nf">index</span>
<span class="n">respond_with</span> <span class="no">Event</span><span class="p">.</span><span class="nf">order</span><span class="p">(</span><span class="ss">event_date: :DESC</span><span class="p">)</span>
<span class="k">end</span>
<span class="k">def</span> <span class="nf">show</span>
<span class="n">respond_with</span> <span class="no">Event</span><span class="p">.</span><span class="nf">find</span><span class="p">(</span><span class="n">params</span><span class="p">[</span><span class="ss">:id</span><span class="p">])</span>
<span class="k">end</span>
<span class="k">def</span> <span class="nf">create</span>
<span class="n">respond_with</span> <span class="ss">:api</span><span class="p">,</span> <span class="no">Event</span><span class="p">.</span><span class="nf">create</span><span class="p">(</span><span class="n">event_params</span><span class="p">)</span>
<span class="k">end</span>
<span class="k">def</span> <span class="nf">destroy</span>
<span class="n">respond_with</span> <span class="no">Event</span><span class="p">.</span><span class="nf">destroy</span><span class="p">(</span><span class="n">params</span><span class="p">[</span><span class="ss">:id</span><span class="p">])</span>
<span class="k">end</span>
<span class="k">def</span> <span class="nf">update</span>
<span class="n">event</span> <span class="o">=</span> <span class="no">Event</span><span class="p">.</span><span class="nf">find</span><span class="p">(</span><span class="n">params</span><span class="p">[</span><span class="s1">'id'</span><span class="p">])</span>
<span class="n">event</span><span class="p">.</span><span class="nf">update</span><span class="p">(</span><span class="n">event_params</span><span class="p">)</span>
<span class="n">respond_with</span> <span class="no">Event</span><span class="p">,</span> <span class="ss">json: </span><span class="n">event</span>
<span class="k">end</span>
<span class="kp">private</span>
<span class="k">def</span> <span class="nf">event_params</span>
<span class="n">params</span><span class="p">.</span><span class="nf">require</span><span class="p">(</span><span class="ss">:event</span><span class="p">).</span><span class="nf">permit</span><span class="p">(</span>
<span class="ss">:id</span><span class="p">,</span>
<span class="ss">:event_type</span><span class="p">,</span>
<span class="ss">:event_date</span><span class="p">,</span>
<span class="ss">:title</span><span class="p">,</span>
<span class="ss">:speaker</span><span class="p">,</span>
<span class="ss">:host</span><span class="p">,</span>
<span class="ss">:published</span>
<span class="p">)</span>
<span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>
<p>Here we start off by declaring that our controller will respond to JSON requests. We then define controller methods corresponding to the CRUD actions we wish to perform, then finish off by listing which parameters may be used for mass assignment.</p>
<p>Notice the use of the <code class="language-plaintext highlighter-rouge">respond_with</code> method which allows us to render a resource as JSON. Without this method, you would write something like:</p>
<div class="language-rb highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">index</span>
<span class="n">respond_to</span> <span class="k">do</span> <span class="o">|</span><span class="nb">format</span><span class="o">|</span>
<span class="nb">format</span><span class="p">.</span><span class="nf">json</span> <span class="p">{</span> <span class="n">render</span> <span class="ss">json: </span><span class="no">Event</span><span class="p">.</span><span class="nf">order</span><span class="p">(</span><span class="ss">event_date: :DESC</span><span class="p">)</span> <span class="p">}</span>
<span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>
<p>Which, applied to the whole controller, would make for quite a bit more code.</p>
<p>The final thing we need to do regarding controllers is to change the forgery protection method in <code class="language-plaintext highlighter-rouge">app/controllers/application_controller.rb</code>:</p>
<div class="language-rb highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">ApplicationController</span> <span class="o"><</span> <span class="no">ActionController</span><span class="o">::</span><span class="no">Base</span>
<span class="n">protect_from_forgery</span> <span class="ss">with: :null_session</span>
<span class="k">end</span>
</code></pre></div></div>
<p>The reason this is necessary is that Rails has a built in mechanism to protect against <a href="https://www.incapsula.com/web-application-security/csrf-cross-site-request-forgery.html">cross site request forgery</a> (CSRF) attacks. By default this sees Rails generate a unique token and validate its authenticity with each POST PUT PATCH DELETE request. If the token is missing, Rails will throw an exception.</p>
<p>However, as we are building a single-page app, we will only have a fresh token upon first render, which means we will need to alter this behaviour. The above code ensures that if no CSRF token is provided, Rails will respond with an empty session, which is fine for our purposes.</p>
<p>If you’d like to read more about this, check out:</p>
<ul>
<li><a href="https://marcgg.com/blog/2016/08/22/csrf-rails/">Understanding Rails’ Forgery Protection Strategies</a></li>
<li><a href="https://medium.com/rubyinside/a-deep-dive-into-csrf-protection-in-rails-19fa0a42c0ef">A Deep Dive into CSRF Protection in Rails</a></li>
<li><a href="https://blog.eq8.eu/article/rails-api-authentication-with-spa-csrf-tokens.html">Rails CSRF protection for SPA</a></li>
<li><a href="https://thinkster.io/tutorials/rails-json-api/configuring-rails-as-a-json-api">Configuring Rails as a JSON API</a></li>
<li><a href="http://jamescpoole.com/2013/10/31/rails-4-csrf-protection-with-clients-using-apis/">Rails 4 CSRF Protection with Clients using APIs</a></li>
</ul>
<h3 id="routes">Routes</h3>
<p>Finally let’s fix up the routes in <code class="language-plaintext highlighter-rouge">config/routes.rb</code>. The routing for the controller has to consider the fact that it’s within the <code class="language-plaintext highlighter-rouge">Api</code> namespace. We’ll do this using the <code class="language-plaintext highlighter-rouge">namespace</code> method.</p>
<div class="language-rb highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">Rails</span><span class="p">.</span><span class="nf">application</span><span class="p">.</span><span class="nf">routes</span><span class="p">.</span><span class="nf">draw</span> <span class="k">do</span>
<span class="n">namespace</span> <span class="ss">:api</span> <span class="k">do</span>
<span class="n">resources</span> <span class="ss">:events</span><span class="p">,</span> <span class="ss">only: </span><span class="sx">%i[index show create destroy update]</span>
<span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>
<p>At this point if you start the server with <code class="language-plaintext highlighter-rouge">rails s</code>, you you can hit the various endpoints and interact with the API. E.g. <a href="http://localhost:3000/api/events.json">http://localhost:3000/api/events.json</a></p>
<p>You might also like to test the API with <a href="https://www.getpostman.com/">Postman</a>. Here’s how you would create a new event.</p>
<p>Set the request type to <em>POST</em>, the URL to <code class="language-plaintext highlighter-rouge">http://localhost:3000/api/events.json</code>, the <em>Headers</em> to <code class="language-plaintext highlighter-rouge">Content-Type: application/json</code> and under <em>Body > raw</em> enter:</p>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w"> </span><span class="nl">"event"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nl">"event_type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Symposium"</span><span class="p">,</span><span class="w">
</span><span class="nl">"event_date"</span><span class="p">:</span><span class="w"> </span><span class="s2">"2019-01-01"</span><span class="p">,</span><span class="w">
</span><span class="nl">"title"</span><span class="p">:</span><span class="w"> </span><span class="s2">"A Symposium"</span><span class="p">,</span><span class="w">
</span><span class="nl">"speaker"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Albert Einstein"</span><span class="p">,</span><span class="w">
</span><span class="nl">"host"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Marie Curie"</span><span class="p">,</span><span class="w">
</span><span class="nl">"published"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span><span class="p">}}</span><span class="w">
</span></code></pre></div></div>
<p><img src="https://res.cloudinary.com/hibbard/image/upload/v1548687631/event-manager/testing-rails-api-with-postman.png" alt="Testing the Rails API with Postman" /></p>
<h2 id="installing-webpacker">Installing Webpacker</h2>
<p>The next thing we want to do is to install the <a href="https://github.com/rails/webpacker">Webpacker gem</a>. This will allow us to use webpack, the JavaScript pre-processor and bundler, to manage application-like JavaScript in Rails.</p>
<p>Add it to your <code class="language-plaintext highlighter-rouge">Gemfile</code>, then run the <code class="language-plaintext highlighter-rouge">bundle</code> command.</p>
<div class="language-rb highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">gem</span> <span class="s1">'webpacker'</span><span class="p">,</span> <span class="s1">'>= 4.0.x'</span>
</code></pre></div></div>
<p>Next, we’re going to need to install <a href="https://yarnpkg.com/en/">Yarn</a>, as this is Webpacker’s package manager of choice.</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>npm i <span class="nt">-g</span> yarn
</code></pre></div></div>
<p>Then run:</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>yarn add @rails/webpacker@next
</code></pre></div></div>
<p>to get the latest version of the <a href="https://www.npmjs.com/package/@rails/webpacker">@rails/webpacker</a> package.</p>
<p>Finally, to bootstrap everything:</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>bundle <span class="nb">exec </span>rails webpacker:install
</code></pre></div></div>
<h3 id="configuring-webpacker">Configuring Webpacker</h3>
<p>Let’s add a <code class="language-plaintext highlighter-rouge">site</code> controller with a blank <code class="language-plaintext highlighter-rouge">index</code> action, and make that the root of our new project.</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">touch </span>app/controllers/site_controller.rb
<span class="nb">mkdir </span>app/views/site
<span class="nb">touch </span>app/views/site/index.html.erb
</code></pre></div></div>
<p>In <code class="language-plaintext highlighter-rouge">app/controllers/site_controller.rb</code>;</p>
<div class="language-rb highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">SiteController</span> <span class="o"><</span> <span class="no">ApplicationController</span>
<span class="k">def</span> <span class="nf">index</span><span class="p">;</span> <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>
<p>In <code class="language-plaintext highlighter-rouge">app/views/site/index.html.erb</code>;</p>
<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt"><div</span> <span class="na">id=</span><span class="s">"root"</span><span class="nt">></div></span>
</code></pre></div></div>
<p>In <code class="language-plaintext highlighter-rouge">config/routes.rb</code>;</p>
<div class="language-rb highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">Rails</span><span class="p">.</span><span class="nf">application</span><span class="p">.</span><span class="nf">routes</span><span class="p">.</span><span class="nf">draw</span> <span class="k">do</span>
<span class="n">root</span> <span class="s1">'site#index'</span>
<span class="n">namespace</span> <span class="ss">:api</span> <span class="k">do</span>
<span class="n">resources</span> <span class="ss">:events</span><span class="p">,</span> <span class="ss">only: </span><span class="sx">%i[index show create destroy update]</span>
<span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>
<p>We’ll also need to include Webpacker’s <code class="language-plaintext highlighter-rouge">javascript_pack_tag</code> in the <code class="language-plaintext highlighter-rouge"><head></code> section of <code class="language-plaintext highlighter-rouge">app/views/layouts/application.html.erb</code>:</p>
<div class="language-erb highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp"><%=</span> <span class="n">javascript_pack_tag</span> <span class="s1">'application'</span> <span class="cp">%></span>
</code></pre></div></div>
<p>Now (re)start the Rails server, hit <a href="http://localhost:3000/">http://localhost:3000/</a> and you should see “Hello World from Webpacker” logged to the console.</p>
<p>This is being generated from <code class="language-plaintext highlighter-rouge">app/javascript/packs/application.js</code>. If you want to assure yourself that Webpacker is working properly, open this file and add some tricky new JavaScript:</p>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="p">{</span><span class="nx">a</span><span class="p">,</span> <span class="nx">b</span><span class="p">,</span> <span class="nx">c</span><span class="p">,</span> <span class="p">...</span><span class="nx">rest</span><span class="p">}</span> <span class="o">=</span> <span class="p">{</span><span class="na">a</span><span class="p">:</span> <span class="mi">1</span><span class="p">,</span> <span class="na">b</span><span class="p">:</span> <span class="mi">2</span><span class="p">,</span> <span class="na">c</span><span class="p">:</span> <span class="mi">3</span><span class="p">,</span> <span class="na">d</span><span class="p">:</span> <span class="mi">4</span><span class="p">,</span> <span class="na">e</span><span class="p">:</span> <span class="mi">5</span><span class="p">};</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">rest</span><span class="p">);</span>
</code></pre></div></div>
<p>Refresh the page and you should see <code class="language-plaintext highlighter-rouge">Object { d: 4, e: 5 }</code> logged to the console. Additionally if you inspect the source, you will notice that this ES2018 syntax has been piped through Babel to produce ES5 compliant JavaScript.</p>
<h3 id="adding-a-basic-react-app">Adding a Basic React App</h3>
<p>This is easy using the built-in installer:</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>bundle <span class="nb">exec </span>rails webpacker:install:react
</code></pre></div></div>
<p>The installer will add all relevant dependencies using Yarn, make the necessary changes to the configuration files, and an create an example React component to your project in <code class="language-plaintext highlighter-rouge">app/javascript/packs</code>.</p>
<p>Then all you need to do is to add <code class="language-plaintext highlighter-rouge"><%= javascript_pack_tag 'hello_react' %></code> to the head of <code class="language-plaintext highlighter-rouge">app/views/layouts/application.html.erb</code> and refresh the page. You should see “Hello React!” output to the screen.</p>
<p>At this point it might be advisable to have a poke around the application to see which extra files Webpacker has created and/or modified. It’s also worth having a peek in <code class="language-plaintext highlighter-rouge">package.json</code> to see what has been installed there.</p>
<h2 id="scaffolding-the-event-manager">Scaffolding the Event Manager</h2>
<p>Next we need to think about how to structure our app’s UI. We’ll start off with an <code class="language-plaintext highlighter-rouge"><Editor></code> component which will contain the following child components:</p>
<ul>
<li>A <code class="language-plaintext highlighter-rouge"><Header></code> component to display our app’s title</li>
<li>An <code class="language-plaintext highlighter-rouge"><EventList></code> component to display a list of events</li>
<li>An <code class="language-plaintext highlighter-rouge"><Event></code> component to display individual events</li>
<li>An <code class="language-plaintext highlighter-rouge"><EventForm></code> component to allow us to edit and create events</li>
</ul>
<p>The whole thing will look like this:</p>
<p><img src="https://res.cloudinary.com/hibbard/image/upload/v1548691454/event-manager/react-app-wireframe.png" alt="React App Wireframe" /></p>
<h2 id="fetching-events">Fetching Events</h2>
<p>Let’s start off by creating all of the files and directories we will need in this section:</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">mkdir </span>app/javascript/components
<span class="nb">touch </span>app/javascript/components/<span class="o">{</span>App.js,Editor.js,Header.js,EventList.js<span class="o">}</span>
</code></pre></div></div>
<blockquote>
<p>Please note that from now on I won’t give the full path of the React components. They are all located in <code class="language-plaintext highlighter-rouge">app/javascript/components</code></p>
</blockquote>
<p>Next, install <a href="https://www.npmjs.com/package/axios">axios</a>, a Promise based HTTP client which we will be using to fetch events from our backend:</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>yarn add axios
</code></pre></div></div>
<p>Remove the <code class="language-plaintext highlighter-rouge"><%= javascript_pack_tag 'hello_react' %></code> from the head of the layout file, and alter <code class="language-plaintext highlighter-rouge">app/javascript/packs/application.js</code> thus:</p>
<div class="language-jsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="nx">React</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">react</span><span class="dl">'</span><span class="p">;</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">render</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">react-dom</span><span class="dl">'</span><span class="p">;</span>
<span class="k">import</span> <span class="nx">App</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">../components/App</span><span class="dl">'</span><span class="p">;</span>
<span class="nb">document</span><span class="p">.</span><span class="nx">addEventListener</span><span class="p">(</span><span class="dl">'</span><span class="s1">DOMContentLoaded</span><span class="dl">'</span><span class="p">,</span> <span class="p">()</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">render</span><span class="p">(<</span><span class="nc">App</span> <span class="p">/>,</span> <span class="nb">document</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="dl">'</span><span class="s1">#root</span><span class="dl">'</span><span class="p">));</span>
<span class="p">});</span>
</code></pre></div></div>
<p>Now we can get on to building the React app. Let’s start off in <code class="language-plaintext highlighter-rouge">App.js</code> where we will require and render our <code class="language-plaintext highlighter-rouge"><Editor></code> component.</p>
<div class="language-jsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="nx">React</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">react</span><span class="dl">'</span><span class="p">;</span>
<span class="k">import</span> <span class="nx">Editor</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">./Editor</span><span class="dl">'</span><span class="p">;</span>
<span class="kd">const</span> <span class="nx">App</span> <span class="o">=</span> <span class="p">()</span> <span class="o">=></span> <span class="p">(</span>
<span class="p"><</span><span class="nt">div</span><span class="p">></span>
<span class="p"><</span><span class="nc">Editor</span> <span class="p">/></span>
<span class="p"></</span><span class="nt">div</span><span class="p">></span>
<span class="p">);</span>
<span class="k">export</span> <span class="k">default</span> <span class="nx">App</span><span class="p">;</span>
</code></pre></div></div>
<p>Next, add the following code to <code class="language-plaintext highlighter-rouge">Editor.js</code>:</p>
<div class="language-jsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="nx">React</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">react</span><span class="dl">'</span><span class="p">;</span>
<span class="k">import</span> <span class="nx">axios</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">axios</span><span class="dl">'</span><span class="p">;</span>
<span class="k">import</span> <span class="nx">Header</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">./Header</span><span class="dl">'</span><span class="p">;</span>
<span class="k">import</span> <span class="nx">EventList</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">./EventList</span><span class="dl">'</span><span class="p">;</span>
<span class="kd">class</span> <span class="nx">Editor</span> <span class="kd">extends</span> <span class="nx">React</span><span class="p">.</span><span class="nx">Component</span> <span class="p">{</span>
<span class="kd">constructor</span><span class="p">(</span><span class="nx">props</span><span class="p">)</span> <span class="p">{</span>
<span class="k">super</span><span class="p">(</span><span class="nx">props</span><span class="p">);</span>
<span class="k">this</span><span class="p">.</span><span class="nx">state</span> <span class="o">=</span> <span class="p">{</span>
<span class="na">events</span><span class="p">:</span> <span class="kc">null</span><span class="p">,</span>
<span class="p">};</span>
<span class="p">}</span>
<span class="nx">componentDidMount</span><span class="p">()</span> <span class="p">{</span>
<span class="nx">axios</span>
<span class="p">.</span><span class="kd">get</span><span class="p">(</span><span class="dl">'</span><span class="s1">/api/events.json</span><span class="dl">'</span><span class="p">)</span>
<span class="p">.</span><span class="nx">then</span><span class="p">(</span><span class="nx">response</span> <span class="o">=></span> <span class="k">this</span><span class="p">.</span><span class="nx">setState</span><span class="p">({</span> <span class="na">events</span><span class="p">:</span> <span class="nx">response</span><span class="p">.</span><span class="nx">data</span> <span class="p">}))</span>
<span class="p">.</span><span class="k">catch</span><span class="p">((</span><span class="nx">error</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">error</span><span class="p">);</span>
<span class="p">});</span>
<span class="p">}</span>
<span class="nx">render</span><span class="p">()</span> <span class="p">{</span>
<span class="kd">const</span> <span class="p">{</span> <span class="nx">events</span> <span class="p">}</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">state</span><span class="p">;</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">events</span> <span class="o">===</span> <span class="kc">null</span><span class="p">)</span> <span class="k">return</span> <span class="kc">null</span><span class="p">;</span>
<span class="k">return</span> <span class="p">(</span>
<span class="p"><</span><span class="nt">div</span><span class="p">></span>
<span class="p"><</span><span class="nc">Header</span> <span class="p">/></span>
<span class="p"><</span><span class="nc">EventList</span> <span class="na">events</span><span class="p">=</span><span class="si">{</span><span class="nx">events</span><span class="si">}</span> <span class="p">/></span>
<span class="p"></</span><span class="nt">div</span><span class="p">></span>
<span class="p">);</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="k">export</span> <span class="k">default</span> <span class="nx">Editor</span><span class="p">;</span>
</code></pre></div></div>
<p>Here we are declaring an <code class="language-plaintext highlighter-rouge">events</code> property in state. Then we are using the <a href="https://reactjs.org/docs/react-component.html#componentdidmount">componentDidMount lifecycle hook</a> to fetch the events from the API. In the <code class="language-plaintext highlighter-rouge">render</code> method we have a guard to make sure nothing is rendered if the events haven’t been fetched. Once they have been fetched however, we are rendering our <code class="language-plaintext highlighter-rouge"><EventList></code> components, passing it a list of events as props.</p>
<p>In <code class="language-plaintext highlighter-rouge">Header.js</code>:</p>
<div class="language-jsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="nx">React</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">react</span><span class="dl">'</span><span class="p">;</span>
<span class="kd">const</span> <span class="nx">Header</span> <span class="o">=</span> <span class="p">()</span> <span class="o">=></span> <span class="p">(</span>
<span class="p"><</span><span class="nt">header</span><span class="p">></span>
<span class="p"><</span><span class="nt">h1</span><span class="p">></span>Event Manager<span class="p"></</span><span class="nt">h1</span><span class="p">></span>
<span class="p"></</span><span class="nt">header</span><span class="p">></span>
<span class="p">);</span>
<span class="k">export</span> <span class="k">default</span> <span class="nx">Header</span><span class="p">;</span>
</code></pre></div></div>
<p>Nothing exciting going on here. We’re just rendering a header element.</p>
<p>In <code class="language-plaintext highlighter-rouge">EventList.js</code> add the following:</p>
<div class="language-jsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="nx">React</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">react</span><span class="dl">'</span><span class="p">;</span>
<span class="k">import</span> <span class="nx">PropTypes</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">prop-types</span><span class="dl">'</span><span class="p">;</span>
<span class="kd">class</span> <span class="nx">EventList</span> <span class="kd">extends</span> <span class="nx">React</span><span class="p">.</span><span class="nx">Component</span> <span class="p">{</span>
<span class="nx">renderEvents</span><span class="p">()</span> <span class="p">{</span>
<span class="kd">const</span> <span class="p">{</span> <span class="nx">events</span> <span class="p">}</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">props</span><span class="p">;</span>
<span class="nx">events</span><span class="p">.</span><span class="nx">sort</span><span class="p">(</span>
<span class="p">(</span><span class="nx">a</span><span class="p">,</span> <span class="nx">b</span><span class="p">)</span> <span class="o">=></span> <span class="k">new</span> <span class="nb">Date</span><span class="p">(</span><span class="nx">b</span><span class="p">.</span><span class="nx">event_date</span><span class="p">)</span> <span class="o">-</span> <span class="k">new</span> <span class="nb">Date</span><span class="p">(</span><span class="nx">a</span><span class="p">.</span><span class="nx">event_date</span><span class="p">),</span>
<span class="p">);</span>
<span class="k">return</span> <span class="nx">events</span><span class="p">.</span><span class="nx">map</span><span class="p">(</span><span class="nx">event</span> <span class="o">=></span> <span class="p">(</span>
<span class="p"><</span><span class="nt">li</span> <span class="na">key</span><span class="p">=</span><span class="si">{</span><span class="nx">event</span><span class="p">.</span><span class="nx">id</span><span class="si">}</span><span class="p">></span>
<span class="si">{</span><span class="nx">event</span><span class="p">.</span><span class="nx">event_date</span><span class="si">}</span>
<span class="si">{</span><span class="dl">'</span><span class="s1"> - </span><span class="dl">'</span><span class="si">}</span>
<span class="si">{</span><span class="nx">event</span><span class="p">.</span><span class="nx">event_type</span><span class="si">}</span>
<span class="p"></</span><span class="nt">li</span><span class="p">></span>
<span class="p">));</span>
<span class="p">}</span>
<span class="nx">render</span><span class="p">()</span> <span class="p">{</span>
<span class="k">return</span> <span class="p">(</span>
<span class="p"><</span><span class="nt">section</span><span class="p">></span>
<span class="p"><</span><span class="nt">h2</span><span class="p">></span>Events<span class="p"></</span><span class="nt">h2</span><span class="p">></span>
<span class="p"><</span><span class="nt">ul</span><span class="p">></span><span class="si">{</span><span class="k">this</span><span class="p">.</span><span class="nx">renderEvents</span><span class="p">()</span><span class="si">}</span><span class="p"></</span><span class="nt">ul</span><span class="p">></span>
<span class="p"></</span><span class="nt">section</span><span class="p">></span>
<span class="p">);</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="nx">EventList</span><span class="p">.</span><span class="nx">propTypes</span> <span class="o">=</span> <span class="p">{</span>
<span class="na">events</span><span class="p">:</span> <span class="nx">PropTypes</span><span class="p">.</span><span class="nx">arrayOf</span><span class="p">(</span><span class="nx">PropTypes</span><span class="p">.</span><span class="nx">object</span><span class="p">),</span>
<span class="p">};</span>
<span class="nx">EventList</span><span class="p">.</span><span class="nx">defaultProps</span> <span class="o">=</span> <span class="p">{</span>
<span class="na">events</span><span class="p">:</span> <span class="p">[],</span>
<span class="p">};</span>
<span class="k">export</span> <span class="k">default</span> <span class="nx">EventList</span><span class="p">;</span>
</code></pre></div></div>
<p>Here we have a <code class="language-plaintext highlighter-rouge">renderEvents</code> method which returns a sorted list of events for the <code class="language-plaintext highlighter-rouge">render</code> method to display. Note that we have also implemented some simple prop validation to ensure that the component is passed an array.</p>
<p>If you visit <a href="http://localhost:3000/">http://localhost:3000</a> you should see a list of events displayed. Exciting, huh?</p>
<p><img src="https://res.cloudinary.com/hibbard/image/upload/v1548707997/event-manager/event-manager-01.png" alt="Event Manager - list of events" /></p>
<h2 id="adding-react-devtools-eslint--webpack-dev-server">Adding React Devtools, ESLint & webpack-dev-server</h2>
<p>Now that we’re writing some JavaScript, it’s a good time to install a couple of tools to aid our development process and to ensure the quality of our code. Let’s start with <a href="https://eslint.org/">ESLint</a>:</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>yarn add eslint <span class="nt">--dev</span>
</code></pre></div></div>
<p>Then add the <a href="https://github.com/airbnb/javascript/tree/master/packages/eslint-config-airbnb">Airbnb config</a> to the project:</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>yarn add eslint-config-airbnb <span class="nt">--dev</span>
</code></pre></div></div>
<p>Next, find out what the remaining dependencies are:</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>npm info <span class="s2">"eslint-config-airbnb@latest"</span> peerDependencies
</code></pre></div></div>
<p>Outputs:</p>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span>
<span class="nl">eslint</span><span class="p">:</span> <span class="dl">'</span><span class="s1">^4.19.1 || ^5.3.0</span><span class="dl">'</span><span class="p">,</span>
<span class="dl">'</span><span class="s1">eslint-plugin-import</span><span class="dl">'</span><span class="p">:</span> <span class="dl">'</span><span class="s1">^2.14.0</span><span class="dl">'</span><span class="p">,</span>
<span class="dl">'</span><span class="s1">eslint-plugin-jsx-a11y</span><span class="dl">'</span><span class="p">:</span> <span class="dl">'</span><span class="s1">^6.1.1</span><span class="dl">'</span><span class="p">,</span>
<span class="dl">'</span><span class="s1">eslint-plugin-react</span><span class="dl">'</span><span class="p">:</span> <span class="dl">'</span><span class="s1">^7.11.0</span><span class="dl">'</span>
<span class="p">}</span>
</code></pre></div></div>
<p>Add the final three packages to the <code class="language-plaintext highlighter-rouge">devDependencies</code> section of <code class="language-plaintext highlighter-rouge">package.json</code> and run <code class="language-plaintext highlighter-rouge">yarn</code>.</p>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nl">"devDependencies"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nl">"eslint"</span><span class="p">:</span><span class="w"> </span><span class="s2">"^5.12.1"</span><span class="p">,</span><span class="w">
</span><span class="nl">"eslint-config-airbnb"</span><span class="p">:</span><span class="w"> </span><span class="s2">"^17.1.0"</span><span class="p">,</span><span class="w">
</span><span class="nl">"eslint-plugin-import"</span><span class="p">:</span><span class="w"> </span><span class="s2">"^2.14.0"</span><span class="p">,</span><span class="w">
</span><span class="nl">"eslint-plugin-jsx-a11y"</span><span class="p">:</span><span class="w"> </span><span class="s2">"^6.1.1"</span><span class="p">,</span><span class="w">
</span><span class="nl">"eslint-plugin-react"</span><span class="p">:</span><span class="w"> </span><span class="s2">"^7.11.0"</span><span class="p">,</span><span class="w">
</span><span class="nl">"webpack-dev-server"</span><span class="p">:</span><span class="w"> </span><span class="s2">"^3.1.14"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>
<p>Create an <code class="language-plaintext highlighter-rouge">.eslintrc.js</code> file in the project root and add:</p>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">module</span><span class="p">.</span><span class="nx">exports</span> <span class="o">=</span> <span class="p">{</span>
<span class="na">extends</span><span class="p">:</span> <span class="dl">'</span><span class="s1">airbnb</span><span class="dl">'</span><span class="p">,</span>
<span class="na">rules</span><span class="p">:</span> <span class="p">{</span>
<span class="dl">'</span><span class="s1">react/jsx-filename-extension</span><span class="dl">'</span><span class="p">:</span> <span class="p">[</span><span class="mi">1</span><span class="p">,</span> <span class="p">{</span> <span class="na">extensions</span><span class="p">:</span> <span class="p">[</span><span class="dl">'</span><span class="s1">.js</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">.jsx</span><span class="dl">'</span><span class="p">]</span> <span class="p">}],</span>
<span class="dl">'</span><span class="s1">no-console</span><span class="dl">'</span><span class="p">:</span> <span class="mi">0</span><span class="p">,</span>
<span class="dl">'</span><span class="s1">no-alert</span><span class="dl">'</span><span class="p">:</span> <span class="mi">0</span><span class="p">,</span>
<span class="p">},</span>
<span class="p">};</span>
</code></pre></div></div>
<p>This will tell ESlint to use the Airbnb ruleset we just installed. It will also allow files with a <code class="language-plaintext highlighter-rouge">js</code> ending to contain JSX and switch off warnings for <code class="language-plaintext highlighter-rouge">console</code> and <code class="language-plaintext highlighter-rouge">alert</code> statements.</p>
<p>You can run ESLint from the terminal:</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>./node_modules/.bin/eslint app/javascript
</code></pre></div></div>
<p>Or as an npm script:</p>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nl">"scripts"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nl">"lint"</span><span class="p">:</span><span class="w"> </span><span class="s2">"eslint app/javascript"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>
<p>But for the best results, you’ll probably want to integrate it into your editor. I’m using Sublime Text 3 with <a href="https://github.com/SublimeLinter/SublimeLinter">SublimeLinter</a>, <a href="https://github.com/SublimeLinter/SublimeLinter-eslint">SublimeLinter-eslint</a> and <a href="https://github.com/TheSavior/ESLint-Formatter">ESLint-Formatter</a> to great effect.</p>
<p>Also, while we are looking at tooling, you might like to take a minute to check out <a href="https://github.com/facebook/react-devtools">React’s Developer Tools</a>. These let you inspect the React component hierarchy, including component props and state and are available as a browser extension (for Chrome and Firefox), and as a standalone app.</p>
<p><img src="https://res.cloudinary.com/hibbard/image/upload/v1548710859/event-manager/event-manager-02.png" alt="React Developer Tools" />
And finally, to make for a better developer experience, let’s start the webpack dev server which comes with the Webpacker gem. To do this, open a second terminal (puma should be running in the first) and from the root of your project, run:</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>./bin/webpack-dev-server
</code></pre></div></div>
<p>Now, whenever changes to any <code class="language-plaintext highlighter-rouge">app/javascript/packs/*.js</code> files are detected, webpack will automatically reload the browser to match.</p>
<h2 id="displaying-an-event">Displaying an Event</h2>
<p>Next, let’s make the events list clickable, so that when a user selects an event, its details are displayed on the screen. For this we’re going to need React router, which will change the URL to reflect the current event and provide us with an outlet for our event information.</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>yarn add react-router-dom
</code></pre></div></div>
<p>Now let’s sort out the routes in <code class="language-plaintext highlighter-rouge">config/routes.rb</code>:</p>
<div class="language-rb highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">Rails</span><span class="p">.</span><span class="nf">application</span><span class="p">.</span><span class="nf">routes</span><span class="p">.</span><span class="nf">draw</span> <span class="k">do</span>
<span class="n">root</span> <span class="ss">to: </span><span class="n">redirect</span><span class="p">(</span><span class="s1">'/events'</span><span class="p">)</span>
<span class="n">get</span> <span class="s1">'events'</span><span class="p">,</span> <span class="ss">to: </span><span class="s1">'site#index'</span>
<span class="n">get</span> <span class="s1">'events/new'</span><span class="p">,</span> <span class="ss">to: </span><span class="s1">'site#index'</span>
<span class="n">get</span> <span class="s1">'events/:id'</span><span class="p">,</span> <span class="ss">to: </span><span class="s1">'site#index'</span>
<span class="n">get</span> <span class="s1">'events/:id/edit'</span><span class="p">,</span> <span class="ss">to: </span><span class="s1">'site#index'</span>
<span class="n">namespace</span> <span class="ss">:api</span> <span class="k">do</span>
<span class="n">resources</span> <span class="ss">:events</span><span class="p">,</span> <span class="ss">only: </span><span class="sx">%i[index show create destroy update]</span>
<span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>
<p>In the first line we’re pointing our root route to <code class="language-plaintext highlighter-rouge">http://localhost:3000/events</code>, this is purely for aesthetic reasons. However in the four lines that follow, you can see that we are informing Rails about the routes we will be using in our React application. This is important, as otherwise if a user requested any of these routes directly (by refreshing the page, for example), Rails would know nothing about them and would respond with a 404. Doing things this way means that Rails can simply serve our React app and let it work out which view to display.</p>
<p>Now let’s add the router to <code class="language-plaintext highlighter-rouge">app/javascript/packs/application.js</code>:</p>
<div class="language-jsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="nx">React</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">react</span><span class="dl">'</span><span class="p">;</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">render</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">react-dom</span><span class="dl">'</span><span class="p">;</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">BrowserRouter</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">react-router-dom</span><span class="dl">'</span><span class="p">;</span>
<span class="k">import</span> <span class="nx">App</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">../components/App</span><span class="dl">'</span><span class="p">;</span>
<span class="nb">document</span><span class="p">.</span><span class="nx">addEventListener</span><span class="p">(</span><span class="dl">'</span><span class="s1">DOMContentLoaded</span><span class="dl">'</span><span class="p">,</span> <span class="p">()</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">render</span><span class="p">(</span>
<span class="p"><</span><span class="nc">BrowserRouter</span><span class="p">></span>
<span class="p"><</span><span class="nc">App</span> <span class="p">/></span>
<span class="p"></</span><span class="nc">BrowserRouter</span><span class="p">>,</span>
<span class="nb">document</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="dl">'</span><span class="s1">#root</span><span class="dl">'</span><span class="p">),</span>
<span class="p">);</span>
<span class="p">});</span>
</code></pre></div></div>
<p>This wraps the app in a <a href="https://reacttraining.com/react-router/web/api/BrowserRouter"><BrowserRouter> component</a>, that uses the HTML5 history API to keep the UI in sync with the URL.</p>
<p>A small change is necessary in <code class="language-plaintext highlighter-rouge">App.js</code>:</p>
<div class="language-jsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="nx">React</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">react</span><span class="dl">'</span><span class="p">;</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">Route</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">react-router-dom</span><span class="dl">'</span><span class="p">;</span>
<span class="k">import</span> <span class="nx">Editor</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">./Editor</span><span class="dl">'</span><span class="p">;</span>
<span class="kd">const</span> <span class="nx">App</span> <span class="o">=</span> <span class="p">()</span> <span class="o">=></span> <span class="p">(</span>
<span class="p"><</span><span class="nt">div</span><span class="p">></span>
<span class="p"><</span><span class="nc">Route</span> <span class="na">path</span><span class="p">=</span><span class="s">"/events/:id?"</span> <span class="na">component</span><span class="p">=</span><span class="si">{</span><span class="nx">Editor</span><span class="si">}</span> <span class="p">/></span>
<span class="p"></</span><span class="nt">div</span><span class="p">></span>
<span class="p">);</span>
<span class="k">export</span> <span class="k">default</span> <span class="nx">App</span><span class="p">;</span>
</code></pre></div></div>
<p>Instead of rendering our <code class="language-plaintext highlighter-rouge"><Editor></code> component directly, we will now use a <a href="https://reacttraining.com/react-router/web/api/Route"><Route> component</a> to render it whenever the browser’s URL matches the route’s path. As we have made the <code class="language-plaintext highlighter-rouge">:id</code> part of the route optional (due to the question mark) and we are pointing our root route at <code class="language-plaintext highlighter-rouge">/events</code>, this will always be the case.</p>
<p>By doing things this way, we will have access to the URL params within the <code class="language-plaintext highlighter-rouge"><Editor></code> component, that will come in handy later on for determining which event we are dealing with.</p>
<p>Next, we’ll need an <code class="language-plaintext highlighter-rouge"><Event></code> component to display the event.</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">touch </span>app/javascript/components/Event.js
</code></pre></div></div>
<p>Then add:</p>
<div class="language-jsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="nx">React</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">react</span><span class="dl">'</span><span class="p">;</span>
<span class="k">import</span> <span class="nx">PropTypes</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">prop-types</span><span class="dl">'</span><span class="p">;</span>
<span class="kd">const</span> <span class="nx">Event</span> <span class="o">=</span> <span class="p">({</span> <span class="nx">event</span> <span class="p">})</span> <span class="o">=></span> <span class="p">(</span>
<span class="p"><</span><span class="nt">div</span><span class="p">></span>
<span class="p"><</span><span class="nt">h2</span><span class="p">></span>
<span class="si">{</span><span class="nx">event</span><span class="p">.</span><span class="nx">event_date</span><span class="si">}</span>
<span class="si">{</span><span class="dl">'</span><span class="s1"> - </span><span class="dl">'</span><span class="si">}</span>
<span class="si">{</span><span class="nx">event</span><span class="p">.</span><span class="nx">event_type</span><span class="si">}</span>
<span class="p"></</span><span class="nt">h2</span><span class="p">></span>
<span class="p"><</span><span class="nt">ul</span><span class="p">></span>
<span class="p"><</span><span class="nt">li</span><span class="p">></span>
<span class="p"><</span><span class="nt">strong</span><span class="p">></span>Type:<span class="p"></</span><span class="nt">strong</span><span class="p">></span>
<span class="si">{</span><span class="dl">'</span><span class="s1"> </span><span class="dl">'</span><span class="si">}</span>
<span class="si">{</span><span class="nx">event</span><span class="p">.</span><span class="nx">event_type</span><span class="si">}</span>
<span class="p"></</span><span class="nt">li</span><span class="p">></span>
<span class="p"><</span><span class="nt">li</span><span class="p">></span>
<span class="p"><</span><span class="nt">strong</span><span class="p">></span>Date:<span class="p"></</span><span class="nt">strong</span><span class="p">></span>
<span class="si">{</span><span class="dl">'</span><span class="s1"> </span><span class="dl">'</span><span class="si">}</span>
<span class="si">{</span><span class="nx">event</span><span class="p">.</span><span class="nx">event_date</span><span class="si">}</span>
<span class="p"></</span><span class="nt">li</span><span class="p">></span>
<span class="p"><</span><span class="nt">li</span><span class="p">></span>
<span class="p"><</span><span class="nt">strong</span><span class="p">></span>Title:<span class="p"></</span><span class="nt">strong</span><span class="p">></span>
<span class="si">{</span><span class="dl">'</span><span class="s1"> </span><span class="dl">'</span><span class="si">}</span>
<span class="si">{</span><span class="nx">event</span><span class="p">.</span><span class="nx">title</span><span class="si">}</span>
<span class="p"></</span><span class="nt">li</span><span class="p">></span>
<span class="p"><</span><span class="nt">li</span><span class="p">></span>
<span class="p"><</span><span class="nt">strong</span><span class="p">></span>Speaker:<span class="p"></</span><span class="nt">strong</span><span class="p">></span>
<span class="si">{</span><span class="dl">'</span><span class="s1"> </span><span class="dl">'</span><span class="si">}</span>
<span class="si">{</span><span class="nx">event</span><span class="p">.</span><span class="nx">speaker</span><span class="si">}</span>
<span class="p"></</span><span class="nt">li</span><span class="p">></span>
<span class="p"><</span><span class="nt">li</span><span class="p">></span>
<span class="p"><</span><span class="nt">strong</span><span class="p">></span>Host:<span class="p"></</span><span class="nt">strong</span><span class="p">></span>
<span class="si">{</span><span class="dl">'</span><span class="s1"> </span><span class="dl">'</span><span class="si">}</span>
<span class="si">{</span><span class="nx">event</span><span class="p">.</span><span class="nx">host</span><span class="si">}</span>
<span class="p"></</span><span class="nt">li</span><span class="p">></span>
<span class="p"><</span><span class="nt">li</span><span class="p">></span>
<span class="p"><</span><span class="nt">strong</span><span class="p">></span>Published:<span class="p"></</span><span class="nt">strong</span><span class="p">></span>
<span class="si">{</span><span class="dl">'</span><span class="s1"> </span><span class="dl">'</span><span class="si">}</span>
<span class="si">{</span><span class="nx">event</span><span class="p">.</span><span class="nx">published</span> <span class="p">?</span> <span class="dl">'</span><span class="s1">yes</span><span class="dl">'</span> <span class="p">:</span> <span class="dl">'</span><span class="s1">no</span><span class="dl">'</span><span class="si">}</span>
<span class="p"></</span><span class="nt">li</span><span class="p">></span>
<span class="p"></</span><span class="nt">ul</span><span class="p">></span>
<span class="p"></</span><span class="nt">div</span><span class="p">></span>
<span class="p">);</span>
<span class="nx">Event</span><span class="p">.</span><span class="nx">propTypes</span> <span class="o">=</span> <span class="p">{</span>
<span class="na">event</span><span class="p">:</span> <span class="nx">PropTypes</span><span class="p">.</span><span class="nx">shape</span><span class="p">(),</span>
<span class="p">};</span>
<span class="nx">Event</span><span class="p">.</span><span class="nx">defaultProps</span> <span class="o">=</span> <span class="p">{</span>
<span class="na">event</span><span class="p">:</span> <span class="kc">undefined</span><span class="p">,</span>
<span class="p">};</span>
<span class="k">export</span> <span class="k">default</span> <span class="nx">Event</span><span class="p">;</span>
</code></pre></div></div>
<p>There’s nothing too wild going on here — this component is expecting to be passed an event object as props and will display it accordingly.</p>
<p>Next, let’s make the list of events in <code class="language-plaintext highlighter-rouge"><EventList></code> clickable. When clicked, they should navigate to <code class="language-plaintext highlighter-rouge">/events/:id</code>.</p>
<div class="language-jsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">Link</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">react-router-dom</span><span class="dl">'</span><span class="p">;</span>
<span class="p">...</span>
<span class="nx">renderEvents</span><span class="p">()</span> <span class="p">{</span>
<span class="p">...</span>
<span class="k">return</span> <span class="nx">events</span><span class="p">.</span><span class="nx">map</span><span class="p">(</span><span class="nx">event</span> <span class="o">=></span> <span class="p">(</span>
<span class="p"><</span><span class="nt">li</span> <span class="na">key</span><span class="p">=</span><span class="si">{</span><span class="nx">event</span><span class="p">.</span><span class="nx">id</span><span class="si">}</span><span class="p">></span>
<span class="p"><</span><span class="nc">Link</span> <span class="na">to</span><span class="p">=</span><span class="si">{</span><span class="s2">`/events/</span><span class="p">${</span><span class="nx">event</span><span class="p">.</span><span class="nx">id</span><span class="p">}</span><span class="s2">`</span><span class="si">}</span><span class="p">></span>
<span class="si">{</span><span class="nx">event</span><span class="p">.</span><span class="nx">event_date</span><span class="si">}</span>
<span class="si">{</span><span class="dl">'</span><span class="s1"> - </span><span class="dl">'</span><span class="si">}</span>
<span class="si">{</span><span class="nx">event</span><span class="p">.</span><span class="nx">event_type</span><span class="si">}</span>
<span class="p"></</span><span class="nc">Link</span><span class="p">></span>
<span class="p"></</span><span class="nt">li</span><span class="p">></span>
<span class="p">));</span>
<span class="p">}</span>
</code></pre></div></div>
<p>Here, we are making use of React router’s <a href="https://reacttraining.com/react-router/web/api/Link"><Link> component</a> to create the navigation around our application.</p>
<p>To make the event display in the correct place, we need to use a further route. In the <code class="language-plaintext highlighter-rouge"><Editor></code> component:</p>
<div class="language-jsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">...</span>
<span class="k">import</span> <span class="nx">PropTypes</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">prop-types</span><span class="dl">'</span><span class="p">;</span>
<span class="k">import</span> <span class="nx">PropsRoute</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">./PropsRoute</span><span class="dl">'</span><span class="p">;</span>
<span class="k">import</span> <span class="nx">Event</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">./Event</span><span class="dl">'</span><span class="p">;</span>
<span class="kd">class</span> <span class="nx">Editor</span> <span class="kd">extends</span> <span class="nx">React</span><span class="p">.</span><span class="nx">Component</span> <span class="p">{</span>
<span class="p">...</span>
<span class="nx">render</span><span class="p">()</span> <span class="p">{</span>
<span class="kd">const</span> <span class="p">{</span> <span class="nx">events</span> <span class="p">}</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">state</span><span class="p">;</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">events</span> <span class="o">===</span> <span class="kc">null</span><span class="p">)</span> <span class="k">return</span> <span class="kc">null</span><span class="p">;</span>
<span class="kd">const</span> <span class="p">{</span> <span class="nx">match</span> <span class="p">}</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">props</span><span class="p">;</span>
<span class="kd">const</span> <span class="nx">eventId</span> <span class="o">=</span> <span class="nx">match</span><span class="p">.</span><span class="nx">params</span><span class="p">.</span><span class="nx">id</span><span class="p">;</span>
<span class="kd">const</span> <span class="nx">event</span> <span class="o">=</span> <span class="nx">events</span><span class="p">.</span><span class="nx">find</span><span class="p">(</span><span class="nx">e</span> <span class="o">=></span> <span class="nx">e</span><span class="p">.</span><span class="nx">id</span> <span class="o">===</span> <span class="nb">Number</span><span class="p">(</span><span class="nx">eventId</span><span class="p">));</span>
<span class="k">return</span> <span class="p">(</span>
<span class="p"><</span><span class="nt">div</span><span class="p">></span>
<span class="p"><</span><span class="nc">Header</span> <span class="p">/></span>
<span class="p"><</span><span class="nc">EventList</span> <span class="na">events</span><span class="p">=</span><span class="si">{</span><span class="nx">events</span><span class="si">}</span> <span class="p">/></span>
<span class="p"><</span><span class="nc">PropsRoute</span> <span class="na">path</span><span class="p">=</span><span class="s">"/events/:id"</span> <span class="na">component</span><span class="p">=</span><span class="si">{</span><span class="nx">Event</span><span class="si">}</span> <span class="na">event</span><span class="p">=</span><span class="si">{</span><span class="nx">event</span><span class="si">}</span> <span class="p">/></span>
<span class="p"></</span><span class="nt">div</span><span class="p">></span>
<span class="p">);</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="nx">Editor</span><span class="p">.</span><span class="nx">propTypes</span> <span class="o">=</span> <span class="p">{</span>
<span class="na">match</span><span class="p">:</span> <span class="nx">PropTypes</span><span class="p">.</span><span class="nx">shape</span><span class="p">(),</span>
<span class="p">};</span>
<span class="nx">Editor</span><span class="p">.</span><span class="nx">defaultProps</span> <span class="o">=</span> <span class="p">{</span>
<span class="na">match</span><span class="p">:</span> <span class="kc">undefined</span><span class="p">,</span>
<span class="p">};</span>
<span class="k">export</span> <span class="k">default</span> <span class="nx">Editor</span><span class="p">;</span>
</code></pre></div></div>
<p>If you look at the <code class="language-plaintext highlighter-rouge">render</code> method, you’ll notice we’re using a new component called <code class="language-plaintext highlighter-rouge"><PropsRoute></code>. This is because when a user selects an event, we want to pass that event to the <code class="language-plaintext highlighter-rouge"><Event></code> component, so that it can display it. Unfortunately, out of the box, React Router doesn’t offer an easy way to pass props to a route, so we’re left to write this ourselves.</p>
<p>Let’s create the component:</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">touch </span>app/javascript/components/PropsRoute.js
</code></pre></div></div>
<p>And add the following:</p>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="nx">React</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">react</span><span class="dl">'</span><span class="p">;</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">Route</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">react-router-dom</span><span class="dl">'</span><span class="p">;</span>
<span class="k">import</span> <span class="nx">PropTypes</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">prop-types</span><span class="dl">'</span><span class="p">;</span>
<span class="kd">const</span> <span class="nx">renderMergedProps</span> <span class="o">=</span> <span class="p">(</span><span class="nx">component</span><span class="p">,</span> <span class="p">...</span><span class="nx">rest</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="kd">const</span> <span class="nx">finalProps</span> <span class="o">=</span> <span class="nb">Object</span><span class="p">.</span><span class="nx">assign</span><span class="p">({},</span> <span class="p">...</span><span class="nx">rest</span><span class="p">);</span>
<span class="k">return</span> <span class="nx">React</span><span class="p">.</span><span class="nx">createElement</span><span class="p">(</span><span class="nx">component</span><span class="p">,</span> <span class="nx">finalProps</span><span class="p">);</span>
<span class="p">};</span>
<span class="kd">const</span> <span class="nx">PropsRoute</span> <span class="o">=</span> <span class="p">({</span> <span class="nx">component</span><span class="p">,</span> <span class="p">...</span><span class="nx">rest</span> <span class="p">})</span> <span class="o">=></span> <span class="p">(</span>
<span class="o"><</span><span class="nx">Route</span> <span class="p">{...</span><span class="nx">rest</span><span class="p">}</span> <span class="nx">render</span><span class="o">=</span><span class="p">{</span><span class="nx">routeProps</span> <span class="o">=></span> <span class="nx">renderMergedProps</span><span class="p">(</span><span class="nx">component</span><span class="p">,</span> <span class="nx">routeProps</span><span class="p">,</span> <span class="nx">rest</span><span class="p">)}</span> <span class="sr">/</span><span class="err">>
</span><span class="p">);</span>
<span class="nx">PropsRoute</span><span class="p">.</span><span class="nx">propTypes</span> <span class="o">=</span> <span class="p">{</span>
<span class="na">component</span><span class="p">:</span> <span class="nx">PropTypes</span><span class="p">.</span><span class="nx">func</span><span class="p">.</span><span class="nx">isRequired</span><span class="p">,</span>
<span class="p">};</span>
<span class="k">export</span> <span class="k">default</span> <span class="nx">PropsRoute</span><span class="p">;</span>
</code></pre></div></div>
<p>This code is taken from here:</p>
<ul>
<li><a href="https://github.com/ReactTraining/react-router/issues/4105#issuecomment-289195202">https://github.com/ReactTraining/react-router/issues/4105#issuecomment-289195202</a></li>
<li><a href="https://github.com/ReactTraining/react-router/blob/v3/examples/passing-props-to-children/app.js">https://github.com/ReactTraining/react-router/blob/v3/examples/passing-props-to-children/app.js</a></li>
</ul>
<p>And now when you click on a link, the correct event should display.</p>
<p><img src="https://res.cloudinary.com/hibbard/image/upload/v1548949067/event-manager/event-manager-03.png" alt="Event Manager - displaying an event" /></p>
<h2 id="disable-turbolinks">Disable Turbolinks</h2>
<p>Now that we have React router up and running, we need to disable Turbolinks, as it messes with the back button’s functionality.</p>
<p>In your <code class="language-plaintext highlighter-rouge">Gemfile</code> remove:</p>
<div class="language-rb highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Turbolinks makes navigating your web application faster. Read more: https://github.com/turbolinks/turbolinks</span>
<span class="n">gem</span> <span class="s1">'turbolinks'</span><span class="p">,</span> <span class="s1">'~> 5'</span>
</code></pre></div></div>
<p>Then run <code class="language-plaintext highlighter-rouge">bundle</code>.</p>
<p>In <code class="language-plaintext highlighter-rouge">app/assets/javascripts/application.js</code> remove:</p>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">//= require turbolinks</span>
</code></pre></div></div>
<p>Finally, in <code class="language-plaintext highlighter-rouge">app/views/layouts/application.html.erb</code> remove:</p>
<div class="language-erb highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp"><%=</span> <span class="n">stylesheet_link_tag</span> <span class="s1">'application'</span><span class="p">,</span> <span class="ss">media: </span><span class="s1">'all'</span><span class="p">,</span> <span class="s1">'data-turbolinks-track'</span><span class="p">:</span> <span class="s1">'reload'</span> <span class="cp">%></span>
<span class="cp"><%=</span> <span class="n">javascript_include_tag</span> <span class="s1">'application'</span><span class="p">,</span> <span class="s1">'data-turbolinks-track'</span><span class="p">:</span> <span class="s1">'reload'</span> <span class="cp">%></span>
</code></pre></div></div>
<p>You can find more info here: <a href="https://stackoverflow.com/questions/38649550/how-to-disable-turbolinks-in-rails-5">https://stackoverflow.com/questions/38649550/how-to-disable-turbolinks-in-rails-5</a></p>
<h2 id="adding-some-styling">Adding Some Styling</h2>
<p>The app looks pretty ugly right now, so let’s brighten it up a little. Create a file named <code class="language-plaintext highlighter-rouge">App.css</code>:</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">touch </span>app/javascript/components/App.css
</code></pre></div></div>
<p>And add the following styles:</p>
<div class="language-css highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">@import</span> <span class="sx">url("https://fonts.googleapis.com/css?family=Roboto:400,700,300,400italic")</span><span class="p">;</span>
<span class="k">@import</span> <span class="sx">url("https://fonts.googleapis.com/css?family=Maven+Pro:400,500,700")</span><span class="p">;</span>
<span class="nt">body</span><span class="o">,</span> <span class="nt">html</span><span class="o">,</span> <span class="nt">div</span><span class="o">,</span> <span class="nt">blockquote</span><span class="o">,</span> <span class="nt">img</span><span class="o">,</span> <span class="nt">label</span><span class="o">,</span> <span class="nt">p</span><span class="o">,</span> <span class="nt">h1</span><span class="o">,</span> <span class="nt">h2</span><span class="o">,</span> <span class="nt">h3</span><span class="o">,</span> <span class="nt">h4</span><span class="o">,</span> <span class="nt">h5</span><span class="o">,</span> <span class="nt">h6</span><span class="o">,</span> <span class="nt">pre</span><span class="o">,</span> <span class="nt">ul</span><span class="o">,</span> <span class="nt">ol</span><span class="o">,</span> <span class="nt">li</span><span class="o">,</span> <span class="nt">dl</span><span class="o">,</span> <span class="nt">dt</span><span class="o">,</span> <span class="nt">dd</span><span class="o">,</span> <span class="nt">form</span><span class="o">,</span> <span class="nt">a</span><span class="o">,</span> <span class="nt">fieldset</span><span class="o">,</span> <span class="nt">input</span><span class="o">,</span> <span class="nt">th</span><span class="o">,</span> <span class="nt">td</span> <span class="p">{</span>
<span class="nl">margin</span><span class="p">:</span> <span class="m">0</span><span class="p">;</span>
<span class="nl">padding</span><span class="p">:</span> <span class="m">0</span><span class="p">;</span>
<span class="p">}</span>
<span class="nt">ul</span><span class="o">,</span> <span class="nt">ol</span> <span class="p">{</span>
<span class="nl">list-style</span><span class="p">:</span> <span class="nb">none</span><span class="p">;</span>
<span class="p">}</span>
<span class="nt">body</span> <span class="p">{</span>
<span class="nl">font-family</span><span class="p">:</span> <span class="n">Roboto</span><span class="p">;</span>
<span class="nl">font-size</span><span class="p">:</span> <span class="m">16px</span><span class="p">;</span>
<span class="nl">line-height</span><span class="p">:</span> <span class="m">28px</span><span class="p">;</span>
<span class="p">}</span>
<span class="nt">header</span> <span class="p">{</span>
<span class="nl">background</span><span class="p">:</span> <span class="m">#f57011</span><span class="p">;</span>
<span class="nl">height</span><span class="p">:</span> <span class="m">60px</span><span class="p">;</span>
<span class="p">}</span>
<span class="nt">header</span> <span class="nt">h1</span><span class="o">,</span> <span class="nt">header</span> <span class="nt">h1</span> <span class="nt">a</span><span class="p">{</span>
<span class="nl">display</span><span class="p">:</span> <span class="n">inline-block</span><span class="p">;</span>
<span class="nl">font-family</span><span class="p">:</span> <span class="s1">"Maven Pro"</span><span class="p">;</span>
<span class="nl">font-size</span><span class="p">:</span> <span class="m">28px</span><span class="p">;</span>
<span class="nl">font-weight</span><span class="p">:</span> <span class="m">500</span><span class="p">;</span>
<span class="nl">color</span><span class="p">:</span> <span class="no">white</span><span class="p">;</span>
<span class="nl">padding</span><span class="p">:</span> <span class="m">14px</span> <span class="m">5%</span><span class="p">;</span>
<span class="nl">text-decoration</span><span class="p">:</span> <span class="nb">none</span><span class="p">;</span>
<span class="p">}</span>
<span class="nt">header</span> <span class="nt">h1</span> <span class="nt">a</span><span class="nd">:hover</span> <span class="p">{</span>
<span class="nl">text-decoration</span><span class="p">:</span> <span class="nb">underline</span><span class="p">;</span>
<span class="p">}</span>
<span class="nc">.grid</span> <span class="p">{</span>
<span class="nl">display</span><span class="p">:</span> <span class="n">grid</span><span class="p">;</span>
<span class="py">grid-gap</span><span class="p">:</span> <span class="m">50px</span><span class="p">;</span>
<span class="py">grid-template-columns</span><span class="p">:</span> <span class="m">25%</span> <span class="nb">auto</span><span class="p">;</span>
<span class="nl">margin</span><span class="p">:</span> <span class="m">25px</span> <span class="nb">auto</span><span class="p">;</span>
<span class="nl">width</span><span class="p">:</span> <span class="m">90%</span><span class="p">;</span>
<span class="nl">height</span><span class="p">:</span> <span class="n">calc</span><span class="p">(</span><span class="m">100vh</span> <span class="n">-</span> <span class="m">145px</span><span class="p">);</span>
<span class="p">}</span>
<span class="nc">.eventList</span> <span class="p">{</span>
<span class="nl">background</span><span class="p">:</span> <span class="m">#f6f6f6</span><span class="p">;</span>
<span class="nl">padding</span><span class="p">:</span> <span class="m">16px</span><span class="p">;</span>
<span class="p">}</span>
<span class="nc">.eventList</span> <span class="nt">h2</span> <span class="p">{</span>
<span class="nl">font-size</span><span class="p">:</span> <span class="m">20px</span><span class="p">;</span>
<span class="nl">padding</span><span class="p">:</span> <span class="m">8px</span> <span class="m">6px</span> <span class="m">10px</span><span class="p">;</span>
<span class="p">}</span>
<span class="nc">.eventContainer</span> <span class="p">{</span>
<span class="nl">font-size</span><span class="p">:</span> <span class="m">15px</span><span class="p">;</span>
<span class="nl">line-height</span><span class="p">:</span> <span class="m">35px</span><span class="p">;</span>
<span class="p">}</span>
<span class="nc">.eventContainer</span> <span class="nt">h2</span> <span class="p">{</span>
<span class="nl">margin-bottom</span><span class="p">:</span> <span class="m">10px</span><span class="p">;</span>
<span class="p">}</span>
<span class="nc">.eventList</span> <span class="nt">li</span><span class="nd">:hover</span><span class="o">,</span> <span class="nt">a</span><span class="nc">.active</span> <span class="p">{</span>
<span class="nl">background</span><span class="p">:</span> <span class="m">#f8e5ce</span><span class="p">;</span>
<span class="p">}</span>
<span class="nc">.eventList</span> <span class="nt">a</span> <span class="p">{</span>
<span class="nl">display</span><span class="p">:</span> <span class="nb">block</span><span class="p">;</span>
<span class="nl">color</span><span class="p">:</span> <span class="no">black</span><span class="p">;</span>
<span class="nl">text-decoration</span><span class="p">:</span> <span class="nb">none</span><span class="p">;</span>
<span class="nl">border-bottom</span><span class="p">:</span> <span class="m">1px</span> <span class="nb">solid</span> <span class="m">#dddddd</span><span class="p">;</span>
<span class="nl">padding</span><span class="p">:</span> <span class="m">8px</span> <span class="m">6px</span> <span class="m">10px</span><span class="p">;</span>
<span class="nl">outline</span><span class="p">:</span> <span class="m">0</span><span class="p">;</span>
<span class="p">}</span>
<span class="nc">.eventList</span> <span class="nt">h2</span> <span class="o">></span> <span class="nt">a</span> <span class="p">{</span>
<span class="nl">color</span><span class="p">:</span> <span class="m">#236fff</span><span class="p">;</span>
<span class="nl">font-size</span><span class="p">:</span> <span class="m">15px</span><span class="p">;</span>
<span class="nl">float</span><span class="p">:</span> <span class="nb">right</span><span class="p">;</span>
<span class="nl">font-weight</span><span class="p">:</span> <span class="nb">normal</span><span class="p">;</span>
<span class="nl">border-bottom</span><span class="p">:</span> <span class="nb">none</span><span class="p">;</span>
<span class="nl">padding</span><span class="p">:</span> <span class="m">0px</span><span class="p">;</span>
<span class="p">}</span>
<span class="nc">.eventForm</span> <span class="p">{</span>
<span class="nl">margin-top</span><span class="p">:</span> <span class="m">15px</span><span class="p">;</span>
<span class="p">}</span>
<span class="nt">label</span> <span class="o">></span> <span class="nt">strong</span> <span class="p">{</span>
<span class="nl">display</span><span class="p">:</span> <span class="n">inline-block</span><span class="p">;</span>
<span class="nl">vertical-align</span><span class="p">:</span> <span class="nb">top</span><span class="p">;</span>
<span class="nl">text-align</span><span class="p">:</span> <span class="nb">right</span><span class="p">;</span>
<span class="nl">width</span><span class="p">:</span> <span class="m">100px</span><span class="p">;</span>
<span class="nl">margin-right</span><span class="p">:</span> <span class="m">6px</span><span class="p">;</span>
<span class="nl">font-size</span><span class="p">:</span> <span class="m">15px</span><span class="p">;</span>
<span class="p">}</span>
<span class="nt">input</span><span class="o">,</span> <span class="nt">textarea</span> <span class="p">{</span>
<span class="nl">padding</span><span class="p">:</span> <span class="m">2px</span> <span class="m">0</span> <span class="m">3px</span> <span class="m">3px</span><span class="p">;</span>
<span class="nl">width</span><span class="p">:</span> <span class="m">400px</span><span class="p">;</span>
<span class="nl">margin-bottom</span><span class="p">:</span> <span class="m">15px</span><span class="p">;</span>
<span class="nl">box-sizing</span><span class="p">:</span> <span class="n">border-box</span><span class="p">;</span>
<span class="p">}</span>
<span class="nt">input</span><span class="o">[</span><span class="nt">type</span><span class="o">=</span><span class="s1">"checkbox"</span><span class="o">]</span> <span class="p">{</span>
<span class="nl">width</span><span class="p">:</span> <span class="m">13px</span><span class="p">;</span>
<span class="p">}</span>
<span class="nt">button</span><span class="o">[</span><span class="nt">type</span><span class="o">=</span><span class="s1">"submit"</span><span class="o">]</span> <span class="p">{</span>
<span class="nl">background</span><span class="p">:</span> <span class="m">#f57011</span><span class="p">;</span>
<span class="nl">border</span><span class="p">:</span> <span class="nb">none</span><span class="p">;</span>
<span class="nl">padding</span><span class="p">:</span> <span class="m">5px</span> <span class="m">25px</span> <span class="m">8px</span><span class="p">;</span>
<span class="nl">font-weight</span><span class="p">:</span> <span class="m">500</span><span class="p">;</span>
<span class="nl">color</span><span class="p">:</span> <span class="no">white</span><span class="p">;</span>
<span class="nl">cursor</span><span class="p">:</span> <span class="nb">pointer</span><span class="p">;</span>
<span class="nl">margin</span><span class="p">:</span> <span class="m">10px</span> <span class="m">0</span> <span class="m">0</span> <span class="m">106px</span><span class="p">;</span>
<span class="p">}</span>
<span class="nc">.errors</span> <span class="p">{</span>
<span class="nl">border</span><span class="p">:</span> <span class="m">1px</span> <span class="nb">solid</span> <span class="no">red</span><span class="p">;</span>
<span class="nl">border-radius</span><span class="p">:</span> <span class="m">5px</span><span class="p">;</span>
<span class="nl">margin</span><span class="p">:</span> <span class="m">20px</span> <span class="m">0</span> <span class="m">35px</span> <span class="m">0</span><span class="p">;</span>
<span class="nl">width</span><span class="p">:</span> <span class="m">513px</span><span class="p">;</span>
<span class="p">}</span>
<span class="nc">.errors</span> <span class="nt">h3</span> <span class="p">{</span>
<span class="nl">background</span><span class="p">:</span> <span class="no">red</span><span class="p">;</span>
<span class="nl">color</span><span class="p">:</span> <span class="no">white</span><span class="p">;</span>
<span class="nl">padding</span><span class="p">:</span> <span class="m">10px</span><span class="p">;</span>
<span class="nl">font-size</span><span class="p">:</span> <span class="m">15px</span><span class="p">;</span>
<span class="p">}</span>
<span class="nc">.errors</span> <span class="nt">ul</span> <span class="nt">li</span> <span class="p">{</span>
<span class="nl">list-style-type</span><span class="p">:</span> <span class="nb">none</span><span class="p">;</span>
<span class="nl">margin</span><span class="p">:</span> <span class="m">0</span><span class="p">;</span>
<span class="nl">padding</span><span class="p">:</span> <span class="m">8px</span> <span class="m">0</span> <span class="m">8px</span> <span class="m">10px</span><span class="p">;</span>
<span class="nl">border-top</span><span class="p">:</span> <span class="nb">solid</span> <span class="m">1px</span> <span class="no">pink</span><span class="p">;</span>
<span class="nl">font-size</span><span class="p">:</span> <span class="m">12px</span><span class="p">;</span>
<span class="nl">font-weight</span><span class="p">:</span> <span class="m">0.9</span><span class="p">;</span>
<span class="p">}</span>
<span class="nt">button</span><span class="nc">.delete</span> <span class="p">{</span>
<span class="nl">background</span><span class="p">:</span> <span class="nb">none</span> <span class="cp">!important</span><span class="p">;</span>
<span class="nl">border</span><span class="p">:</span> <span class="nb">none</span><span class="p">;</span>
<span class="nl">padding</span><span class="p">:</span> <span class="m">0</span> <span class="cp">!important</span><span class="p">;</span>
<span class="nl">margin-left</span><span class="p">:</span> <span class="m">10px</span><span class="p">;</span>
<span class="nl">cursor</span><span class="p">:</span> <span class="nb">pointer</span><span class="p">;</span>
<span class="nl">color</span><span class="p">:</span> <span class="m">#236fff</span><span class="p">;</span>
<span class="nl">font-size</span><span class="p">:</span> <span class="m">15px</span><span class="p">;</span>
<span class="nl">font-weight</span><span class="p">:</span> <span class="nb">normal</span><span class="p">;</span>
<span class="nl">margin</span><span class="p">:</span> <span class="m">3px</span> <span class="m">0</span> <span class="m">0</span> <span class="m">0</span><span class="p">;</span>
<span class="nl">text-decoration</span><span class="p">:</span> <span class="nb">none</span><span class="p">;</span>
<span class="p">}</span>
<span class="nt">button</span><span class="nc">.delete</span><span class="nd">:hover</span> <span class="p">{</span>
<span class="nl">text-decoration</span><span class="p">:</span> <span class="nb">underline</span><span class="p">;</span>
<span class="p">}</span>
<span class="nt">h2</span> <span class="nt">a</span> <span class="p">{</span>
<span class="nl">color</span><span class="p">:</span> <span class="m">#236fff</span><span class="p">;</span>
<span class="nl">font-size</span><span class="p">:</span> <span class="m">15px</span><span class="p">;</span>
<span class="nl">font-weight</span><span class="p">:</span> <span class="nb">normal</span><span class="p">;</span>
<span class="nl">margin</span><span class="p">:</span> <span class="m">3px</span> <span class="m">12px</span> <span class="m">0</span> <span class="m">12px</span><span class="p">;</span>
<span class="nl">text-decoration</span><span class="p">:</span> <span class="nb">none</span><span class="p">;</span>
<span class="p">}</span>
<span class="nt">h2</span> <span class="nt">a</span><span class="nd">:hover</span> <span class="p">{</span>
<span class="nl">text-decoration</span><span class="p">:</span> <span class="nb">underline</span><span class="p">;</span>
<span class="p">}</span>
<span class="nc">.form-actions</span> <span class="nt">a</span> <span class="p">{</span>
<span class="nl">color</span><span class="p">:</span> <span class="m">#236fff</span><span class="p">;</span>
<span class="nl">font-size</span><span class="p">:</span> <span class="m">15px</span><span class="p">;</span>
<span class="nl">margin</span><span class="p">:</span> <span class="m">3px</span> <span class="m">12px</span> <span class="m">0</span> <span class="m">12px</span><span class="p">;</span>
<span class="nl">text-decoration</span><span class="p">:</span> <span class="nb">none</span><span class="p">;</span>
<span class="p">}</span>
<span class="nc">.form-actions</span> <span class="nt">a</span><span class="nd">:hover</span> <span class="p">{</span>
<span class="nl">text-decoration</span><span class="p">:</span> <span class="nb">underline</span><span class="p">;</span>
<span class="p">}</span>
<span class="nt">input</span><span class="nc">.search</span> <span class="p">{</span>
<span class="nl">width</span><span class="p">:</span> <span class="m">92%</span><span class="p">;</span>
<span class="nl">margin</span><span class="p">:</span> <span class="m">15px</span> <span class="m">2px</span><span class="p">;</span>
<span class="nl">padding</span><span class="p">:</span> <span class="m">4px</span> <span class="m">0</span> <span class="m">6px</span> <span class="m">6px</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>
<blockquote>
<p>Please note that these are all of the styles we will need in the app. Listing them all in one go is intended to keep the article a tad shorter.</p>
</blockquote>
<p>Here, we’re using a small <a href="https://code.tutsplus.com/tutorials/quick-tip-create-your-own-simple-resetcss-file--net-206">custom reset</a> and the goodness of CSS grid for our layout. If you’re unfamiliar with CSS grid, there’s a good tutorial here: <a href="https://medialoot.com/blog/a-beginners-guide-to-css-grid-layout/">https://medialoot.com/blog/a-beginners-guide-to-css-grid-layout/</a></p>
<p>Import our styles in <code class="language-plaintext highlighter-rouge">App.js</code>:</p>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="dl">'</span><span class="s1">./App.css</span><span class="dl">'</span><span class="p">;</span>
</code></pre></div></div>
<p>Next, add some markup to the <code class="language-plaintext highlighter-rouge"><Editor></code> component:</p>
<div class="language-jsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">render</span><span class="p">()</span> <span class="p">{</span>
<span class="p">...</span>
<span class="k">return</span> <span class="p">(</span>
<span class="p"><</span><span class="nt">div</span><span class="p">></span>
<span class="p"><</span><span class="nc">Header</span> <span class="p">/></span>
<span class="p"><</span><span class="nt">div</span> <span class="na">className</span><span class="p">=</span><span class="s">"grid"</span><span class="p">></span>
<span class="p"><</span><span class="nc">EventList</span> <span class="na">events</span><span class="p">=</span><span class="si">{</span><span class="nx">events</span><span class="si">}</span> <span class="p">/></span>
<span class="p"><</span><span class="nc">PropsRoute</span> <span class="na">path</span><span class="p">=</span><span class="s">"/events/:id"</span> <span class="na">component</span><span class="p">=</span><span class="si">{</span><span class="nx">Event</span><span class="si">}</span> <span class="na">event</span><span class="p">=</span><span class="si">{</span><span class="nx">event</span><span class="si">}</span> <span class="p">/></span>
<span class="p"></</span><span class="nt">div</span><span class="p">></span>
<span class="p"></</span><span class="nt">div</span><span class="p">></span>
<span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>
<p>The <code class="language-plaintext highlighter-rouge"><EventList></code> component:</p>
<div class="language-jsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">render</span><span class="p">()</span> <span class="p">{</span>
<span class="k">return</span> <span class="p">(</span>
<span class="p"><</span><span class="nt">section</span> <span class="na">className</span><span class="p">=</span><span class="s">"eventList"</span><span class="p">></span>
<span class="p"><</span><span class="nt">h2</span><span class="p">></span>Events<span class="p"></</span><span class="nt">h2</span><span class="p">></span>
<span class="p"><</span><span class="nt">ul</span><span class="p">></span><span class="si">{</span><span class="k">this</span><span class="p">.</span><span class="nx">renderEvents</span><span class="p">()</span><span class="si">}</span><span class="p"></</span><span class="nt">ul</span><span class="p">></span>
<span class="p"></</span><span class="nt">section</span><span class="p">></span>
<span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>
<p>And the <code class="language-plaintext highlighter-rouge"><Event></code> component:</p>
<div class="language-jsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">Event</span> <span class="o">=</span> <span class="p">({</span> <span class="nx">event</span> <span class="p">})</span> <span class="o">=></span> <span class="p">(</span>
<span class="p"><</span><span class="nt">div</span> <span class="na">className</span><span class="p">=</span><span class="s">"eventContainer"</span><span class="p">></span>
...
<span class="p"></</span><span class="nt">div</span><span class="p">></span>
<span class="p">);</span>
</code></pre></div></div>
<p>Now everything should be styled nicely.</p>
<p><img src="https://res.cloudinary.com/hibbard/image/upload/v1548950164/event-manager/event-manager-04.png" alt="A nicely styled Event Manager" /></p>
<h2 id="adding-a-class-to-the-selected-event">Adding a Class to the Selected Event</h2>
<p>Now it would be nice to add some kind of indication that a user has selected an event. This isn’t too tricky. In the <code class="language-plaintext highlighter-rouge"><Editor></code> component pass in an <code class="language-plaintext highlighter-rouge">activeID</code> prop to the <code class="language-plaintext highlighter-rouge"><EventList></code> component.</p>
<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt"><EventList</span> <span class="na">events=</span><span class="s">{events}</span> <span class="na">activeId=</span><span class="s">{Number(eventId)}</span> <span class="nt">/></span>
</code></pre></div></div>
<p>In the <code class="language-plaintext highlighter-rouge"><EventList></code> component, use this to apply a class of <code class="language-plaintext highlighter-rouge">active</code> when rendering the list.</p>
<div class="language-jsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">renderEvents</span><span class="p">()</span> <span class="p">{</span>
<span class="kd">const</span> <span class="p">{</span> <span class="nx">activeId</span><span class="p">,</span> <span class="nx">events</span> <span class="p">}</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">props</span><span class="p">;</span>
<span class="nx">events</span><span class="p">.</span><span class="nx">sort</span><span class="p">((</span><span class="nx">a</span><span class="p">,</span> <span class="nx">b</span><span class="p">)</span> <span class="o">=></span> <span class="k">new</span> <span class="nb">Date</span><span class="p">(</span><span class="nx">b</span><span class="p">.</span><span class="nx">event_date</span><span class="p">)</span> <span class="o">-</span> <span class="k">new</span> <span class="nb">Date</span><span class="p">(</span><span class="nx">a</span><span class="p">.</span><span class="nx">event_date</span><span class="p">));</span>
<span class="k">return</span> <span class="nx">events</span><span class="p">.</span><span class="nx">map</span><span class="p">(</span><span class="nx">event</span> <span class="o">=></span> <span class="p">(</span>
<span class="p"><</span><span class="nt">li</span> <span class="na">key</span><span class="p">=</span><span class="si">{</span><span class="nx">event</span><span class="p">.</span><span class="nx">id</span><span class="si">}</span><span class="p">></span>
<span class="p"><</span><span class="nc">Link</span> <span class="na">to</span><span class="p">=</span><span class="si">{</span><span class="s2">`/events/</span><span class="p">${</span><span class="nx">event</span><span class="p">.</span><span class="nx">id</span><span class="p">}</span><span class="s2">`</span><span class="si">}</span> <span class="na">className</span><span class="p">=</span><span class="si">{</span><span class="nx">activeId</span> <span class="o">===</span> <span class="nx">event</span><span class="p">.</span><span class="nx">id</span> <span class="p">?</span> <span class="dl">'</span><span class="s1">active</span><span class="dl">'</span> <span class="p">:</span> <span class="dl">''</span><span class="si">}</span><span class="p">></span>
<span class="si">{</span><span class="nx">event</span><span class="p">.</span><span class="nx">event_date</span><span class="si">}</span>
<span class="si">{</span><span class="dl">'</span><span class="s1"> - </span><span class="dl">'</span><span class="si">}</span>
<span class="si">{</span><span class="nx">event</span><span class="p">.</span><span class="nx">event_type</span><span class="si">}</span>
<span class="p"></</span><span class="nc">Link</span><span class="p">></span>
<span class="p"></</span><span class="nt">li</span><span class="p">></span>
<span class="p">));</span>
<span class="p">}</span>
<span class="p">...</span>
<span class="nx">EventList</span><span class="p">.</span><span class="nx">propTypes</span> <span class="o">=</span> <span class="p">{</span>
<span class="na">activeId</span><span class="p">:</span> <span class="nx">PropTypes</span><span class="p">.</span><span class="nx">number</span><span class="p">,</span>
<span class="na">events</span><span class="p">:</span> <span class="nx">PropTypes</span><span class="p">.</span><span class="nx">arrayOf</span><span class="p">(</span><span class="nx">PropTypes</span><span class="p">.</span><span class="nx">object</span><span class="p">),</span>
<span class="p">};</span>
<span class="nx">EventList</span><span class="p">.</span><span class="nx">defaultProps</span> <span class="o">=</span> <span class="p">{</span>
<span class="na">activeId</span><span class="p">:</span> <span class="kc">undefined</span><span class="p">,</span>
<span class="na">events</span><span class="p">:</span> <span class="p">[],</span>
<span class="p">};</span>
</code></pre></div></div>
<p>We already added some CSS styles for the active link, so when you now click on an event, the details should be displayed and the event should be highlighted accordingly.</p>
<h2 id="creating-an-event">Creating an Event</h2>
<p>So far we have the <em>Read</em> functionality of our CRUD app. Now let’s add the ability to create an event.</p>
<p>Start off in the <code class="language-plaintext highlighter-rouge"><Editor></code> component:</p>
<div class="language-jsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="nx">React</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">react</span><span class="dl">'</span><span class="p">;</span>
<span class="p">...</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">Switch</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">react-router-dom</span><span class="dl">'</span><span class="p">;</span>
<span class="k">import</span> <span class="nx">EventForm</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">./EventForm</span><span class="dl">'</span><span class="p">;</span>
<span class="kd">class</span> <span class="nx">Editor</span> <span class="kd">extends</span> <span class="nx">React</span><span class="p">.</span><span class="nx">Component</span> <span class="p">{</span>
<span class="p">...</span>
<span class="nx">render</span><span class="p">()</span> <span class="p">{</span>
<span class="p">...</span>
<span class="k">return</span> <span class="p">(</span>
<span class="p"><</span><span class="nt">div</span><span class="p">></span>
<span class="p"><</span><span class="nc">Header</span> <span class="p">/></span>
<span class="p"><</span><span class="nt">div</span> <span class="na">className</span><span class="p">=</span><span class="s">"grid"</span><span class="p">></span>
<span class="p"><</span><span class="nc">EventList</span> <span class="na">events</span><span class="p">=</span><span class="si">{</span><span class="nx">events</span><span class="si">}</span> <span class="na">activeId</span><span class="p">=</span><span class="si">{</span><span class="nb">Number</span><span class="p">(</span><span class="nx">eventId</span><span class="p">)</span><span class="si">}</span> <span class="p">/></span>
<span class="p"><</span><span class="nc">Switch</span><span class="p">></span>
<span class="p"><</span><span class="nc">PropsRoute</span> <span class="na">path</span><span class="p">=</span><span class="s">"/events/new"</span> <span class="na">component</span><span class="p">=</span><span class="si">{</span><span class="nx">EventForm</span><span class="si">}</span> <span class="p">/></span>
<span class="p"><</span><span class="nc">PropsRoute</span> <span class="na">path</span><span class="p">=</span><span class="s">"/events/:id"</span> <span class="na">component</span><span class="p">=</span><span class="si">{</span><span class="nx">Event</span><span class="si">}</span> <span class="na">event</span><span class="p">=</span><span class="si">{</span><span class="nx">event</span><span class="si">}</span> <span class="p">/></span>
<span class="p"></</span><span class="nc">Switch</span><span class="p">></span>
<span class="p"></</span><span class="nt">div</span><span class="p">></span>
<span class="p"></</span><span class="nt">div</span><span class="p">></span>
<span class="p">);</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<p>Here, we’ve introduced a <a href="https://reacttraining.com/react-router/web/api/Switch"><Switch> component</a>, which will render the first child <code class="language-plaintext highlighter-rouge"><Route></code> that matches the location. This is practical, as we don’t want the new event form and the <code class="language-plaintext highlighter-rouge"><Event></code> component to display at once.</p>
<p>We’ll add a link to display the form in the <code class="language-plaintext highlighter-rouge"><EventList></code> component:</p>
<div class="language-jsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">render</span><span class="p">()</span> <span class="p">{</span>
<span class="k">return</span> <span class="p">(</span>
<span class="p"><</span><span class="nt">section</span> <span class="na">className</span><span class="p">=</span><span class="s">"eventList"</span><span class="p">></span>
<span class="p"><</span><span class="nt">h2</span><span class="p">></span>
Events
<span class="p"><</span><span class="nc">Link</span> <span class="na">to</span><span class="p">=</span><span class="s">"/events/new"</span><span class="p">></span>New Event<span class="p"></</span><span class="nc">Link</span><span class="p">></span>
<span class="p"></</span><span class="nt">h2</span><span class="p">></span>
<span class="p"><</span><span class="nt">ul</span><span class="p">></span><span class="si">{</span><span class="k">this</span><span class="p">.</span><span class="nx">renderEvents</span><span class="p">()</span><span class="si">}</span><span class="p"></</span><span class="nt">ul</span><span class="p">></span>
<span class="p"></</span><span class="nt">section</span><span class="p">></span>
<span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>
<p>Now, let’s create the <code class="language-plaintext highlighter-rouge"><EventForm></code> component:</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">touch </span>app/javascript/components/EventForm.js
</code></pre></div></div>
<p>And add the following content:</p>
<div class="language-jsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="nx">React</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">react</span><span class="dl">'</span><span class="p">;</span>
<span class="kd">class</span> <span class="nx">EventForm</span> <span class="kd">extends</span> <span class="nx">React</span><span class="p">.</span><span class="nx">Component</span> <span class="p">{</span>
<span class="kd">constructor</span><span class="p">(</span><span class="nx">props</span><span class="p">)</span> <span class="p">{</span>
<span class="k">super</span><span class="p">(</span><span class="nx">props</span><span class="p">);</span>
<span class="k">this</span><span class="p">.</span><span class="nx">handleSubmit</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">handleSubmit</span><span class="p">.</span><span class="nx">bind</span><span class="p">(</span><span class="k">this</span><span class="p">);</span>
<span class="p">}</span>
<span class="nx">handleSubmit</span><span class="p">(</span><span class="nx">e</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">e</span><span class="p">.</span><span class="nx">preventDefault</span><span class="p">();</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="dl">'</span><span class="s1">submitted</span><span class="dl">'</span><span class="p">);</span>
<span class="p">}</span>
<span class="nx">render</span><span class="p">()</span> <span class="p">{</span>
<span class="k">return</span> <span class="p">(</span>
<span class="p"><</span><span class="nt">div</span><span class="p">></span>
<span class="p"><</span><span class="nt">h2</span><span class="p">></span>New Event<span class="p"></</span><span class="nt">h2</span><span class="p">></span>
<span class="p"><</span><span class="nt">form</span> <span class="na">className</span><span class="p">=</span><span class="s">"eventForm"</span> <span class="na">onSubmit</span><span class="p">=</span><span class="si">{</span><span class="k">this</span><span class="p">.</span><span class="nx">handleSubmit</span><span class="si">}</span><span class="p">></span>
<span class="p"><</span><span class="nt">div</span><span class="p">></span>
<span class="p"><</span><span class="nt">label</span> <span class="na">htmlFor</span><span class="p">=</span><span class="s">"event_type"</span><span class="p">></span>
<span class="p"><</span><span class="nt">strong</span><span class="p">></span>Type:<span class="p"></</span><span class="nt">strong</span><span class="p">></span>
<span class="p"><</span><span class="nt">input</span> <span class="na">type</span><span class="p">=</span><span class="s">"text"</span> <span class="na">id</span><span class="p">=</span><span class="s">"event_type"</span> <span class="na">name</span><span class="p">=</span><span class="s">"event_type"</span> <span class="p">/></span>
<span class="p"></</span><span class="nt">label</span><span class="p">></span>
<span class="p"></</span><span class="nt">div</span><span class="p">></span>
<span class="p"><</span><span class="nt">div</span><span class="p">></span>
<span class="p"><</span><span class="nt">label</span> <span class="na">htmlFor</span><span class="p">=</span><span class="s">"event_date"</span><span class="p">></span>
<span class="p"><</span><span class="nt">strong</span><span class="p">></span>Date:<span class="p"></</span><span class="nt">strong</span><span class="p">></span>
<span class="p"><</span><span class="nt">input</span> <span class="na">type</span><span class="p">=</span><span class="s">"text"</span> <span class="na">id</span><span class="p">=</span><span class="s">"event_date"</span> <span class="na">name</span><span class="p">=</span><span class="s">"event_date"</span> <span class="p">/></span>
<span class="p"></</span><span class="nt">label</span><span class="p">></span>
<span class="p"></</span><span class="nt">div</span><span class="p">></span>
<span class="p"><</span><span class="nt">div</span><span class="p">></span>
<span class="p"><</span><span class="nt">label</span> <span class="na">htmlFor</span><span class="p">=</span><span class="s">"title"</span><span class="p">></span>
<span class="p"><</span><span class="nt">strong</span><span class="p">></span>Title:<span class="p"></</span><span class="nt">strong</span><span class="p">></span>
<span class="p"><</span><span class="nt">textarea</span> <span class="na">cols</span><span class="p">=</span><span class="s">"30"</span> <span class="na">rows</span><span class="p">=</span><span class="s">"10"</span> <span class="na">id</span><span class="p">=</span><span class="s">"title"</span> <span class="na">name</span><span class="p">=</span><span class="s">"title"</span> <span class="p">/></span>
<span class="p"></</span><span class="nt">label</span><span class="p">></span>
<span class="p"></</span><span class="nt">div</span><span class="p">></span>
<span class="p"><</span><span class="nt">div</span><span class="p">></span>
<span class="p"><</span><span class="nt">label</span> <span class="na">htmlFor</span><span class="p">=</span><span class="s">"speaker"</span><span class="p">></span>
<span class="p"><</span><span class="nt">strong</span><span class="p">></span>Speakers:<span class="p"></</span><span class="nt">strong</span><span class="p">></span>
<span class="p"><</span><span class="nt">input</span> <span class="na">type</span><span class="p">=</span><span class="s">"text"</span> <span class="na">id</span><span class="p">=</span><span class="s">"speaker"</span> <span class="na">name</span><span class="p">=</span><span class="s">"speaker"</span> <span class="p">/></span>
<span class="p"></</span><span class="nt">label</span><span class="p">></span>
<span class="p"></</span><span class="nt">div</span><span class="p">></span>
<span class="p"><</span><span class="nt">div</span><span class="p">></span>
<span class="p"><</span><span class="nt">label</span> <span class="na">htmlFor</span><span class="p">=</span><span class="s">"host"</span><span class="p">></span>
<span class="p"><</span><span class="nt">strong</span><span class="p">></span>Hosts:<span class="p"></</span><span class="nt">strong</span><span class="p">></span>
<span class="p"><</span><span class="nt">input</span> <span class="na">type</span><span class="p">=</span><span class="s">"text"</span> <span class="na">id</span><span class="p">=</span><span class="s">"host"</span> <span class="na">name</span><span class="p">=</span><span class="s">"host"</span> <span class="p">/></span>
<span class="p"></</span><span class="nt">label</span><span class="p">></span>
<span class="p"></</span><span class="nt">div</span><span class="p">></span>
<span class="p"><</span><span class="nt">div</span><span class="p">></span>
<span class="p"><</span><span class="nt">label</span> <span class="na">htmlFor</span><span class="p">=</span><span class="s">"published"</span><span class="p">></span>
<span class="p"><</span><span class="nt">strong</span><span class="p">></span>Publish:<span class="p"></</span><span class="nt">strong</span><span class="p">></span>
<span class="p"><</span><span class="nt">input</span> <span class="na">type</span><span class="p">=</span><span class="s">"checkbox"</span> <span class="na">id</span><span class="p">=</span><span class="s">"published"</span> <span class="na">name</span><span class="p">=</span><span class="s">"published"</span> <span class="p">/></span>
<span class="p"></</span><span class="nt">label</span><span class="p">></span>
<span class="p"></</span><span class="nt">div</span><span class="p">></span>
<span class="p"><</span><span class="nt">div</span> <span class="na">className</span><span class="p">=</span><span class="s">"form-actions"</span><span class="p">></span>
<span class="p"><</span><span class="nt">button</span> <span class="na">type</span><span class="p">=</span><span class="s">"submit"</span><span class="p">></span>Save<span class="p"></</span><span class="nt">button</span><span class="p">></span>
<span class="p"></</span><span class="nt">div</span><span class="p">></span>
<span class="p"></</span><span class="nt">form</span><span class="p">></span>
<span class="p"></</span><span class="nt">div</span><span class="p">></span>
<span class="p">);</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="k">export</span> <span class="k">default</span> <span class="nx">EventForm</span><span class="p">;</span>
</code></pre></div></div>
<p>At this point the form should appear and when you click <em>Save</em>, it should log “Submitted” to the console.</p>
<h2 id="form-validation">Form Validation</h2>
<p>Now, let’s add in some validation to make sure all of the fields (apart from <code class="language-plaintext highlighter-rouge">published</code>) are filled out. All of the action will take place in the <code class="language-plaintext highlighter-rouge"><EventForm></code> component.</p>
<div class="language-jsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="nx">React</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">react</span><span class="dl">'</span><span class="p">;</span>
<span class="k">import</span> <span class="nx">PropTypes</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">prop-types</span><span class="dl">'</span><span class="p">;</span>
<span class="kd">class</span> <span class="nx">EventForm</span> <span class="kd">extends</span> <span class="nx">React</span><span class="p">.</span><span class="nx">Component</span> <span class="p">{</span>
<span class="kd">constructor</span><span class="p">(</span><span class="nx">props</span><span class="p">)</span> <span class="p">{</span>
<span class="k">super</span><span class="p">(</span><span class="nx">props</span><span class="p">);</span>
<span class="k">this</span><span class="p">.</span><span class="nx">state</span> <span class="o">=</span> <span class="p">{</span>
<span class="na">event</span><span class="p">:</span> <span class="nx">props</span><span class="p">.</span><span class="nx">event</span><span class="p">,</span>
<span class="na">errors</span><span class="p">:</span> <span class="p">{},</span>
<span class="p">};</span>
<span class="k">this</span><span class="p">.</span><span class="nx">handleSubmit</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">handleSubmit</span><span class="p">.</span><span class="nx">bind</span><span class="p">(</span><span class="k">this</span><span class="p">);</span>
<span class="k">this</span><span class="p">.</span><span class="nx">handleInputChange</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">handleInputChange</span><span class="p">.</span><span class="nx">bind</span><span class="p">(</span><span class="k">this</span><span class="p">);</span>
<span class="p">}</span>
<span class="nx">handleSubmit</span><span class="p">(</span><span class="nx">e</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">e</span><span class="p">.</span><span class="nx">preventDefault</span><span class="p">();</span>
<span class="kd">const</span> <span class="p">{</span> <span class="nx">event</span> <span class="p">}</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">state</span><span class="p">;</span>
<span class="kd">const</span> <span class="nx">errors</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">validateEvent</span><span class="p">(</span><span class="nx">event</span><span class="p">);</span>
<span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="k">this</span><span class="p">.</span><span class="nx">isEmptyObject</span><span class="p">(</span><span class="nx">errors</span><span class="p">))</span> <span class="p">{</span>
<span class="k">this</span><span class="p">.</span><span class="nx">setState</span><span class="p">({</span> <span class="nx">errors</span> <span class="p">});</span>
<span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">event</span><span class="p">);</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="nx">validateEvent</span><span class="p">(</span><span class="nx">event</span><span class="p">)</span> <span class="p">{</span>
<span class="kd">const</span> <span class="nx">errors</span> <span class="o">=</span> <span class="p">{};</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">event</span><span class="p">.</span><span class="nx">event_type</span> <span class="o">===</span> <span class="dl">''</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">errors</span><span class="p">.</span><span class="nx">event_type</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">You must enter an event type</span><span class="dl">'</span><span class="p">;</span>
<span class="p">}</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">event</span><span class="p">.</span><span class="nx">event_date</span> <span class="o">===</span> <span class="dl">''</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">errors</span><span class="p">.</span><span class="nx">event_date</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">You must enter a valid date</span><span class="dl">'</span><span class="p">;</span>
<span class="p">}</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">event</span><span class="p">.</span><span class="nx">title</span> <span class="o">===</span> <span class="dl">''</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">errors</span><span class="p">.</span><span class="nx">title</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">You must enter a title</span><span class="dl">'</span><span class="p">;</span>
<span class="p">}</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">event</span><span class="p">.</span><span class="nx">speaker</span> <span class="o">===</span> <span class="dl">''</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">errors</span><span class="p">.</span><span class="nx">speaker</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">You must enter at least one speaker</span><span class="dl">'</span><span class="p">;</span>
<span class="p">}</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">event</span><span class="p">.</span><span class="nx">host</span> <span class="o">===</span> <span class="dl">''</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">errors</span><span class="p">.</span><span class="nx">host</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">You must enter at least one host</span><span class="dl">'</span><span class="p">;</span>
<span class="p">}</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">event</span><span class="p">);</span>
<span class="k">return</span> <span class="nx">errors</span><span class="p">;</span>
<span class="p">}</span>
<span class="nx">isEmptyObject</span><span class="p">(</span><span class="nx">obj</span><span class="p">)</span> <span class="p">{</span>
<span class="k">return</span> <span class="nb">Object</span><span class="p">.</span><span class="nx">keys</span><span class="p">(</span><span class="nx">obj</span><span class="p">).</span><span class="nx">length</span> <span class="o">===</span> <span class="mi">0</span><span class="p">;</span>
<span class="p">}</span>
<span class="nx">handleInputChange</span><span class="p">(</span><span class="nx">event</span><span class="p">)</span> <span class="p">{</span>
<span class="kd">const</span> <span class="p">{</span> <span class="nx">target</span> <span class="p">}</span> <span class="o">=</span> <span class="nx">event</span><span class="p">;</span>
<span class="kd">const</span> <span class="p">{</span> <span class="nx">name</span> <span class="p">}</span> <span class="o">=</span> <span class="nx">target</span><span class="p">;</span>
<span class="kd">const</span> <span class="nx">value</span> <span class="o">=</span> <span class="nx">target</span><span class="p">.</span><span class="nx">type</span> <span class="o">===</span> <span class="dl">'</span><span class="s1">checkbox</span><span class="dl">'</span> <span class="p">?</span> <span class="nx">target</span><span class="p">.</span><span class="nx">checked</span> <span class="p">:</span> <span class="nx">target</span><span class="p">.</span><span class="nx">value</span><span class="p">;</span>
<span class="k">this</span><span class="p">.</span><span class="nx">setState</span><span class="p">(</span><span class="nx">prevState</span> <span class="o">=></span> <span class="p">({</span>
<span class="na">event</span><span class="p">:</span> <span class="p">{</span>
<span class="p">...</span><span class="nx">prevState</span><span class="p">.</span><span class="nx">event</span><span class="p">,</span>
<span class="p">[</span><span class="nx">name</span><span class="p">]:</span> <span class="nx">value</span><span class="p">,</span>
<span class="p">},</span>
<span class="p">}));</span>
<span class="p">}</span>
<span class="nx">renderErrors</span><span class="p">()</span> <span class="p">{</span>
<span class="kd">const</span> <span class="p">{</span> <span class="nx">errors</span> <span class="p">}</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">state</span><span class="p">;</span>
<span class="k">if</span> <span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="nx">isEmptyObject</span><span class="p">(</span><span class="nx">errors</span><span class="p">))</span> <span class="p">{</span>
<span class="k">return</span> <span class="kc">null</span><span class="p">;</span>
<span class="p">}</span>
<span class="k">return</span> <span class="p">(</span>
<span class="p"><</span><span class="nt">div</span> <span class="na">className</span><span class="p">=</span><span class="s">"errors"</span><span class="p">></span>
<span class="p"><</span><span class="nt">h3</span><span class="p">></span>The following errors prohibited the event from being saved:<span class="p"></</span><span class="nt">h3</span><span class="p">></span>
<span class="p"><</span><span class="nt">ul</span><span class="p">></span>
<span class="si">{</span><span class="nb">Object</span><span class="p">.</span><span class="nx">values</span><span class="p">(</span><span class="nx">errors</span><span class="p">).</span><span class="nx">map</span><span class="p">(</span><span class="nx">error</span> <span class="o">=></span> <span class="p">(</span>
<span class="p"><</span><span class="nt">li</span> <span class="na">key</span><span class="p">=</span><span class="si">{</span><span class="nx">error</span><span class="si">}</span><span class="p">></span><span class="si">{</span><span class="nx">error</span><span class="si">}</span><span class="p"></</span><span class="nt">li</span><span class="p">></span>
<span class="p">))</span><span class="si">}</span>
<span class="p"></</span><span class="nt">ul</span><span class="p">></span>
<span class="p"></</span><span class="nt">div</span><span class="p">></span>
<span class="p">);</span>
<span class="p">}</span>
<span class="nx">render</span><span class="p">()</span> <span class="p">{</span>
<span class="k">return</span> <span class="p">(</span>
<span class="p"><</span><span class="nt">div</span><span class="p">></span>
<span class="p"><</span><span class="nt">h2</span><span class="p">></span>New Event<span class="p"></</span><span class="nt">h2</span><span class="p">></span>
<span class="si">{</span><span class="k">this</span><span class="p">.</span><span class="nx">renderErrors</span><span class="p">()</span><span class="si">}</span>
<span class="p"><</span><span class="nt">form</span> <span class="na">className</span><span class="p">=</span><span class="s">"eventForm"</span> <span class="na">onSubmit</span><span class="p">=</span><span class="si">{</span><span class="k">this</span><span class="p">.</span><span class="nx">handleSubmit</span><span class="si">}</span><span class="p">></span>
<span class="p"><</span><span class="nt">div</span><span class="p">></span>
<span class="p"><</span><span class="nt">label</span> <span class="na">htmlFor</span><span class="p">=</span><span class="s">"event_type"</span><span class="p">></span>
<span class="p"><</span><span class="nt">strong</span><span class="p">></span>Type:<span class="p"></</span><span class="nt">strong</span><span class="p">></span>
<span class="p"><</span><span class="nt">input</span>
<span class="na">type</span><span class="p">=</span><span class="s">"text"</span>
<span class="na">id</span><span class="p">=</span><span class="s">"event_type"</span>
<span class="na">name</span><span class="p">=</span><span class="s">"event_type"</span>
<span class="na">onChange</span><span class="p">=</span><span class="si">{</span><span class="k">this</span><span class="p">.</span><span class="nx">handleInputChange</span><span class="si">}</span>
<span class="p">/></span>
<span class="p"></</span><span class="nt">label</span><span class="p">></span>
<span class="p"></</span><span class="nt">div</span><span class="p">></span>
<span class="p"><</span><span class="nt">div</span><span class="p">></span>
<span class="p"><</span><span class="nt">label</span> <span class="na">htmlFor</span><span class="p">=</span><span class="s">"event_date"</span><span class="p">></span>
<span class="p"><</span><span class="nt">strong</span><span class="p">></span>Date:<span class="p"></</span><span class="nt">strong</span><span class="p">></span>
<span class="p"><</span><span class="nt">input</span>
<span class="na">type</span><span class="p">=</span><span class="s">"text"</span>
<span class="na">id</span><span class="p">=</span><span class="s">"event_date"</span>
<span class="na">name</span><span class="p">=</span><span class="s">"event_date"</span>
<span class="na">onChange</span><span class="p">=</span><span class="si">{</span><span class="k">this</span><span class="p">.</span><span class="nx">handleInputChange</span><span class="si">}</span>
<span class="p">/></span>
<span class="p"></</span><span class="nt">label</span><span class="p">></span>
<span class="p"></</span><span class="nt">div</span><span class="p">></span>
<span class="p"><</span><span class="nt">div</span><span class="p">></span>
<span class="p"><</span><span class="nt">label</span> <span class="na">htmlFor</span><span class="p">=</span><span class="s">"title"</span><span class="p">></span>
<span class="p"><</span><span class="nt">strong</span><span class="p">></span>Title:<span class="p"></</span><span class="nt">strong</span><span class="p">></span>
<span class="p"><</span><span class="nt">textarea</span>
<span class="na">cols</span><span class="p">=</span><span class="s">"30"</span>
<span class="na">rows</span><span class="p">=</span><span class="s">"10"</span>
<span class="na">id</span><span class="p">=</span><span class="s">"title"</span>
<span class="na">name</span><span class="p">=</span><span class="s">"title"</span>
<span class="na">onChange</span><span class="p">=</span><span class="si">{</span><span class="k">this</span><span class="p">.</span><span class="nx">handleInputChange</span><span class="si">}</span>
<span class="p">/></span>
<span class="p"></</span><span class="nt">label</span><span class="p">></span>
<span class="p"></</span><span class="nt">div</span><span class="p">></span>
<span class="p"><</span><span class="nt">div</span><span class="p">></span>
<span class="p"><</span><span class="nt">label</span> <span class="na">htmlFor</span><span class="p">=</span><span class="s">"speaker"</span><span class="p">></span>
<span class="p"><</span><span class="nt">strong</span><span class="p">></span>Speakers:<span class="p"></</span><span class="nt">strong</span><span class="p">></span>
<span class="p"><</span><span class="nt">input</span> <span class="na">type</span><span class="p">=</span><span class="s">"text"</span> <span class="na">id</span><span class="p">=</span><span class="s">"speaker"</span> <span class="na">name</span><span class="p">=</span><span class="s">"speaker"</span> <span class="na">onChange</span><span class="p">=</span><span class="si">{</span><span class="k">this</span><span class="p">.</span><span class="nx">handleInputChange</span><span class="si">}</span> <span class="p">/></span>
<span class="p"></</span><span class="nt">label</span><span class="p">></span>
<span class="p"></</span><span class="nt">div</span><span class="p">></span>
<span class="p"><</span><span class="nt">div</span><span class="p">></span>
<span class="p"><</span><span class="nt">label</span> <span class="na">htmlFor</span><span class="p">=</span><span class="s">"host"</span><span class="p">></span>
<span class="p"><</span><span class="nt">strong</span><span class="p">></span>Hosts:<span class="p"></</span><span class="nt">strong</span><span class="p">></span>
<span class="p"><</span><span class="nt">input</span> <span class="na">type</span><span class="p">=</span><span class="s">"text"</span> <span class="na">id</span><span class="p">=</span><span class="s">"host"</span> <span class="na">name</span><span class="p">=</span><span class="s">"host"</span> <span class="na">onChange</span><span class="p">=</span><span class="si">{</span><span class="k">this</span><span class="p">.</span><span class="nx">handleInputChange</span><span class="si">}</span> <span class="p">/></span>
<span class="p"></</span><span class="nt">label</span><span class="p">></span>
<span class="p"></</span><span class="nt">div</span><span class="p">></span>
<span class="p"><</span><span class="nt">div</span><span class="p">></span>
<span class="p"><</span><span class="nt">label</span> <span class="na">htmlFor</span><span class="p">=</span><span class="s">"published"</span><span class="p">></span>
<span class="p"><</span><span class="nt">strong</span><span class="p">></span>Publish:<span class="p"></</span><span class="nt">strong</span><span class="p">></span>
<span class="p"><</span><span class="nt">input</span>
<span class="na">type</span><span class="p">=</span><span class="s">"checkbox"</span>
<span class="na">id</span><span class="p">=</span><span class="s">"published"</span>
<span class="na">name</span><span class="p">=</span><span class="s">"published"</span>
<span class="na">onChange</span><span class="p">=</span><span class="si">{</span><span class="k">this</span><span class="p">.</span><span class="nx">handleInputChange</span><span class="si">}</span>
<span class="p">/></span>
<span class="p"></</span><span class="nt">label</span><span class="p">></span>
<span class="p"></</span><span class="nt">div</span><span class="p">></span>
<span class="p"><</span><span class="nt">div</span> <span class="na">className</span><span class="p">=</span><span class="s">"form-actions"</span><span class="p">></span>
<span class="p"><</span><span class="nt">button</span> <span class="na">type</span><span class="p">=</span><span class="s">"submit"</span><span class="p">></span>Save<span class="p"></</span><span class="nt">button</span><span class="p">></span>
<span class="p"></</span><span class="nt">div</span><span class="p">></span>
<span class="p"></</span><span class="nt">form</span><span class="p">></span>
<span class="p"></</span><span class="nt">div</span><span class="p">></span>
<span class="p">);</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="nx">EventForm</span><span class="p">.</span><span class="nx">propTypes</span> <span class="o">=</span> <span class="p">{</span>
<span class="na">event</span><span class="p">:</span> <span class="nx">PropTypes</span><span class="p">.</span><span class="nx">shape</span><span class="p">(),</span>
<span class="p">};</span>
<span class="nx">EventForm</span><span class="p">.</span><span class="nx">defaultProps</span> <span class="o">=</span> <span class="p">{</span>
<span class="na">event</span><span class="p">:</span> <span class="p">{</span>
<span class="na">event_type</span><span class="p">:</span> <span class="dl">''</span><span class="p">,</span>
<span class="na">event_date</span><span class="p">:</span> <span class="dl">''</span><span class="p">,</span>
<span class="na">title</span><span class="p">:</span> <span class="dl">''</span><span class="p">,</span>
<span class="na">speaker</span><span class="p">:</span> <span class="dl">''</span><span class="p">,</span>
<span class="na">host</span><span class="p">:</span> <span class="dl">''</span><span class="p">,</span>
<span class="na">published</span><span class="p">:</span> <span class="kc">false</span><span class="p">,</span>
<span class="p">},</span>
<span class="p">};</span>
<span class="k">export</span> <span class="k">default</span> <span class="nx">EventForm</span><span class="p">;</span>
</code></pre></div></div>
<p>We start off by defining two properties on state: <code class="language-plaintext highlighter-rouge">event</code> and <code class="language-plaintext highlighter-rouge">errors</code>. The <code class="language-plaintext highlighter-rouge">event</code> property is taken from props (you’ll see why in a bit) and for now it is assigned some sensible defaults at the bottom of the file. The <code class="language-plaintext highlighter-rouge">errors</code> property is initialized as an empty object.</p>
<p>Within the <code class="language-plaintext highlighter-rouge">render</code> method, we add an <code class="language-plaintext highlighter-rouge">onChange</code> property to all our form inputs, which we bind to a new <code class="language-plaintext highlighter-rouge">handleInputChange</code> method. This method will update the <code class="language-plaintext highlighter-rouge">event</code> object we are holding in state, so that at any given time, <code class="language-plaintext highlighter-rouge">this.state.event</code> should mirror what has been entered into the form.</p>
<p>We can then expand our <code class="language-plaintext highlighter-rouge">handleSubmit</code> method to check for errors when the form is submitted. In our case, this will simply be that each field has a value. If any errors are detected, the <code class="language-plaintext highlighter-rouge">error</code> object that we are holding in state is updated and the errors are output to the page.</p>
<p>To perform the validation, we are relying on two further methods: <code class="language-plaintext highlighter-rouge">validateEvent</code> and <code class="language-plaintext highlighter-rouge">isEmptyObject</code>. If you have ESLint installed, you will see that it complains that:</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Expected <span class="s1">'this'</span> to be used by class method <span class="s1">'validateEvent'</span><span class="nb">.</span>
Expected <span class="s1">'this'</span> to be used by class method <span class="s1">'isEmptyObject'</span><span class="nb">.</span>
</code></pre></div></div>
<p>This makes them good candidates to move into a helper module. Let’s create that now:</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">mkdir </span>app/javascript/helpers
<span class="nb">touch </span>app/javascript/helpers/helpers.js
</code></pre></div></div>
<p>Now add the following code to <code class="language-plaintext highlighter-rouge">helpers.js</code>, making sure to remove <code class="language-plaintext highlighter-rouge">validateEvent</code> and <code class="language-plaintext highlighter-rouge">isEmptyObject</code> from the <code class="language-plaintext highlighter-rouge"><EventForm></code> component:</p>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">export</span> <span class="kd">const</span> <span class="nx">isEmptyObject</span> <span class="o">=</span> <span class="nx">obj</span> <span class="o">=></span> <span class="nb">Object</span><span class="p">.</span><span class="nx">keys</span><span class="p">(</span><span class="nx">obj</span><span class="p">).</span><span class="nx">length</span> <span class="o">===</span> <span class="mi">0</span><span class="p">;</span>
<span class="k">export</span> <span class="kd">const</span> <span class="nx">validateEvent</span> <span class="o">=</span> <span class="p">(</span><span class="nx">event</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="kd">const</span> <span class="nx">errors</span> <span class="o">=</span> <span class="p">{};</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">event</span><span class="p">.</span><span class="nx">event_type</span> <span class="o">===</span> <span class="dl">''</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">errors</span><span class="p">.</span><span class="nx">event_type</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">You must enter an event type</span><span class="dl">'</span><span class="p">;</span>
<span class="p">}</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">event</span><span class="p">.</span><span class="nx">event_date</span> <span class="o">===</span> <span class="dl">''</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">errors</span><span class="p">.</span><span class="nx">event_date</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">You must enter a valid date</span><span class="dl">'</span><span class="p">;</span>
<span class="p">}</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">event</span><span class="p">.</span><span class="nx">title</span> <span class="o">===</span> <span class="dl">''</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">errors</span><span class="p">.</span><span class="nx">title</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">You must enter a title</span><span class="dl">'</span><span class="p">;</span>
<span class="p">}</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">event</span><span class="p">.</span><span class="nx">speaker</span> <span class="o">===</span> <span class="dl">''</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">errors</span><span class="p">.</span><span class="nx">speaker</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">You must enter at least one speaker</span><span class="dl">'</span><span class="p">;</span>
<span class="p">}</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">event</span><span class="p">.</span><span class="nx">host</span> <span class="o">===</span> <span class="dl">''</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">errors</span><span class="p">.</span><span class="nx">host</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">You must enter at least one host</span><span class="dl">'</span><span class="p">;</span>
<span class="p">}</span>
<span class="k">return</span> <span class="nx">errors</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>
<p>And adjust in the <code class="language-plaintext highlighter-rouge"><EventForm></code> component.</p>
<div class="language-jsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">isEmptyObject</span><span class="p">,</span> <span class="nx">validateEvent</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">../helpers/helpers</span><span class="dl">'</span><span class="p">;</span>
<span class="nx">handleSubmit</span><span class="p">(</span><span class="nx">e</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">e</span><span class="p">.</span><span class="nx">preventDefault</span><span class="p">();</span>
<span class="kd">const</span> <span class="p">{</span> <span class="nx">event</span> <span class="p">}</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">state</span><span class="p">;</span>
<span class="kd">const</span> <span class="nx">errors</span> <span class="o">=</span> <span class="nx">validateEvent</span><span class="p">(</span><span class="nx">event</span><span class="p">);</span>
<span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">isEmptyObject</span><span class="p">(</span><span class="nx">errors</span><span class="p">))</span> <span class="p">{</span>
<span class="k">this</span><span class="p">.</span><span class="nx">setState</span><span class="p">({</span> <span class="nx">errors</span> <span class="p">});</span>
<span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">event</span><span class="p">);</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="nx">renderErrors</span><span class="p">()</span> <span class="p">{</span>
<span class="kd">const</span> <span class="p">{</span> <span class="nx">errors</span> <span class="p">}</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">state</span><span class="p">;</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">isEmptyObject</span><span class="p">(</span><span class="nx">errors</span><span class="p">))</span> <span class="p">{</span>
<span class="k">return</span> <span class="kc">null</span><span class="p">;</span>
<span class="p">}</span>
<span class="p">...</span>
<span class="p">}</span>
</code></pre></div></div>
<p>Now when you attempt to submit a form which is not properly filled out, you should see some nicely formatted errors.</p>
<p><img src="https://res.cloudinary.com/hibbard/image/upload/v1548962054/event-manager/event-manager-05.png" alt="Event Manager - Form submission errors" /></p>
<h2 id="making-the-date-field-a-datepicker">Making the Date Field a Datepicker</h2>
<p>The next thing to do is to wire up our date field as a datepicker. For this we’ll use <a href="https://github.com/Pikaday/Pikaday">Pikaday</a>.</p>
<p>First, we need to install the library from npm:</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>yarn add pikaday
</code></pre></div></div>
<p>Then in the <code class="language-plaintext highlighter-rouge"><EventForm></code> component, import the library:</p>
<div class="language-jsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="nx">Pikaday</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">pikaday</span><span class="dl">'</span><span class="p">;</span>
<span class="k">import</span> <span class="dl">'</span><span class="s1">pikaday/css/pikaday.css</span><span class="dl">'</span><span class="p">;</span>
</code></pre></div></div>
<p>And change the date field like so:</p>
<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt"><div></span>
<span class="nt"><label</span> <span class="na">htmlFor=</span><span class="s">"event_date"</span><span class="nt">></span>
<span class="nt"><strong></span>Date:<span class="nt"></strong></span>
<span class="nt"><input</span>
<span class="na">type=</span><span class="s">"text"</span>
<span class="na">id=</span><span class="s">"event_date"</span>
<span class="na">name=</span><span class="s">"event_date"</span>
<span class="na">ref=</span><span class="s">{this.dateInput}</span>
<span class="na">autoComplete=</span><span class="s">"off"</span>
<span class="nt">/></span>
<span class="nt"></label></span>
<span class="nt"></div></span>
</code></pre></div></div>
<p>As you notice, we are creating a ref on the input so we can reference it elsewhere in the code.</p>
<p>In the constructor add:</p>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">this</span><span class="p">.</span><span class="nx">dateInput</span> <span class="o">=</span> <span class="nx">React</span><span class="p">.</span><span class="nx">createRef</span><span class="p">();</span>
</code></pre></div></div>
<p>You can read more about refs here: <a href="https://reactjs.org/docs/refs-and-the-dom.html">https://reactjs.org/docs/refs-and-the-dom.html</a></p>
<p>Now, in the <code class="language-plaintext highlighter-rouge">componentDidMount</code> lifecycle hook, we need to initialize Pikaday:</p>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">componentDidMount</span><span class="p">()</span> <span class="p">{</span>
<span class="k">new</span> <span class="nx">Pikaday</span><span class="p">({</span>
<span class="na">field</span><span class="p">:</span> <span class="k">this</span><span class="p">.</span><span class="nx">dateInput</span><span class="p">.</span><span class="nx">current</span><span class="p">,</span>
<span class="na">onSelect</span><span class="p">:</span> <span class="p">(</span><span class="nx">date</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="kd">const</span> <span class="nx">formattedDate</span> <span class="o">=</span> <span class="nx">formatDate</span><span class="p">(</span><span class="nx">date</span><span class="p">);</span>
<span class="k">this</span><span class="p">.</span><span class="nx">dateInput</span><span class="p">.</span><span class="nx">current</span><span class="p">.</span><span class="nx">value</span> <span class="o">=</span> <span class="nx">formattedDate</span><span class="p">;</span>
<span class="k">this</span><span class="p">.</span><span class="nx">updateEvent</span><span class="p">(</span><span class="dl">'</span><span class="s1">event_date</span><span class="dl">'</span><span class="p">,</span> <span class="nx">formattedDate</span><span class="p">);</span>
<span class="p">},</span>
<span class="p">});</span>
<span class="p">}</span>
</code></pre></div></div>
<p>Thanks to our ref, the field property of the configuration object that we are passing to Pikaday’s constructor, points to DOM element we want to turn into a datepicker. The <code class="language-plaintext highlighter-rouge">onSelect</code> method determines what will happen when the user selects a date. In this case, the date is formatted into a YYYY-MM-DD string and the <code class="language-plaintext highlighter-rouge">event</code> object we are holding in state is updated.</p>
<p>We can write the <code class="language-plaintext highlighter-rouge">formatDate</code> function as a helper method in <code class="language-plaintext highlighter-rouge">app/javascript/helpers/helpers.js</code>. This receives a <code class="language-plaintext highlighter-rouge">Date</code> object and returns a YYYY-MM-DD string.</p>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">export</span> <span class="kd">const</span> <span class="nx">formatDate</span> <span class="o">=</span> <span class="p">(</span><span class="nx">d</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="kd">const</span> <span class="nx">YYYY</span> <span class="o">=</span> <span class="nx">d</span><span class="p">.</span><span class="nx">getFullYear</span><span class="p">();</span>
<span class="kd">const</span> <span class="nx">MM</span> <span class="o">=</span> <span class="s2">`0</span><span class="p">${</span><span class="nx">d</span><span class="p">.</span><span class="nx">getMonth</span><span class="p">()</span> <span class="o">+</span> <span class="mi">1</span><span class="p">}</span><span class="s2">`</span><span class="p">.</span><span class="nx">slice</span><span class="p">(</span><span class="o">-</span><span class="mi">2</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">DD</span> <span class="o">=</span> <span class="s2">`0</span><span class="p">${</span><span class="nx">d</span><span class="p">.</span><span class="nx">getDate</span><span class="p">()}</span><span class="s2">`</span><span class="p">.</span><span class="nx">slice</span><span class="p">(</span><span class="o">-</span><span class="mi">2</span><span class="p">);</span>
<span class="k">return</span> <span class="s2">`</span><span class="p">${</span><span class="nx">YYYY</span><span class="p">}</span><span class="s2">-</span><span class="p">${</span><span class="nx">MM</span><span class="p">}</span><span class="s2">-</span><span class="p">${</span><span class="nx">DD</span><span class="p">}</span><span class="s2">`</span><span class="p">;</span>
<span class="p">};</span>
</code></pre></div></div>
<p>Don’t forget to import it in the <code class="language-plaintext highlighter-rouge"><EventForm></code> component:</p>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">formatDate</span><span class="p">,</span> <span class="nx">isEmptyObject</span><span class="p">,</span> <span class="nx">validateEvent</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">../helpers/helpers</span><span class="dl">'</span><span class="p">;</span>
</code></pre></div></div>
<p>We can declare the <code class="language-plaintext highlighter-rouge">updateEvent</code> method in the <code class="language-plaintext highlighter-rouge"><EventForm></code> component:</p>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">updateEvent</span><span class="p">(</span><span class="nx">key</span><span class="p">,</span> <span class="nx">value</span><span class="p">)</span> <span class="p">{</span>
<span class="k">this</span><span class="p">.</span><span class="nx">setState</span><span class="p">(</span><span class="nx">prevState</span> <span class="o">=></span> <span class="p">({</span>
<span class="na">event</span><span class="p">:</span> <span class="p">{</span>
<span class="p">...</span><span class="nx">prevState</span><span class="p">.</span><span class="nx">event</span><span class="p">,</span>
<span class="p">[</span><span class="nx">key</span><span class="p">]:</span> <span class="nx">value</span><span class="p">,</span>
<span class="p">},</span>
<span class="p">}));</span>
<span class="p">}</span>
</code></pre></div></div>
<p>We can also use this to dry up our <code class="language-plaintext highlighter-rouge">handleInputChange</code> method:</p>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">handleInputChange</span><span class="p">(</span><span class="nx">event</span><span class="p">)</span> <span class="p">{</span>
<span class="kd">const</span> <span class="p">{</span> <span class="nx">target</span> <span class="p">}</span> <span class="o">=</span> <span class="nx">event</span><span class="p">;</span>
<span class="kd">const</span> <span class="p">{</span> <span class="nx">name</span> <span class="p">}</span> <span class="o">=</span> <span class="nx">target</span><span class="p">;</span>
<span class="kd">const</span> <span class="nx">value</span> <span class="o">=</span> <span class="nx">target</span><span class="p">.</span><span class="nx">type</span> <span class="o">===</span> <span class="dl">'</span><span class="s1">checkbox</span><span class="dl">'</span> <span class="p">?</span> <span class="nx">target</span><span class="p">.</span><span class="nx">checked</span> <span class="p">:</span> <span class="nx">target</span><span class="p">.</span><span class="nx">value</span><span class="p">;</span>
<span class="k">this</span><span class="p">.</span><span class="nx">updateEvent</span><span class="p">(</span><span class="nx">name</span><span class="p">,</span> <span class="nx">value</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>
<p>And that’s it. We now have a datepicker.</p>
<p>Ref.: <a href="https://stackoverflow.com/questions/30058477/how-can-i-use-pikaday-with-reactjs">https://stackoverflow.com/questions/30058477/how-can-i-use-pikaday-with-reactjs</a></p>
<h2 id="warning-in-webpack-console">Warning in Webpack Console</h2>
<p>At the time of writing, the current version of Pikaday is 1.8.0 and this version will cause two warnings to be shown in the webpack console.</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>WARNING <span class="k">in</span> ./node_modules/pikaday/pikaday.js 28:17-24
Critical dependency: the request of a dependency is an expression
WARNING <span class="k">in</span> ./node_modules/pikaday/pikaday.js
Module not found: Error: Can<span class="s1">'t resolve '</span>moment<span class="s1">' in '</span>/home/jim/files/Web design/React/react-event-manager/next/node_modules/pikaday<span class="s1">'
</span></code></pre></div></div>
<p>The first is caused by the way Pikaday includes the Moment library, which it has now made an optional dependency. You can read more about this <a href="https://github.com/webpack/webpack/issues/196">here</a> and <a href="https://github.com/Pikaday/Pikaday/issues/518">here</a>.</p>
<p>The second is caused by Pikaday having made Moment an optional dependency. You can read more about this <a href="https://github.com/Pikaday/Pikaday/issues/814">here</a>.</p>
<p>If the warnings bother you, you can get rid of them by commenting out the Moment requires in <code class="language-plaintext highlighter-rouge">node_modules/pikaday/pikaday.js</code>:</p>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">(</span><span class="kd">function</span> <span class="p">(</span><span class="nx">root</span><span class="p">,</span> <span class="nx">factory</span><span class="p">)</span>
<span class="p">{</span>
<span class="dl">'</span><span class="s1">use strict</span><span class="dl">'</span><span class="p">;</span>
<span class="kd">var</span> <span class="nx">moment</span><span class="p">;</span>
<span class="k">if</span> <span class="p">(</span><span class="k">typeof</span> <span class="nx">exports</span> <span class="o">===</span> <span class="dl">'</span><span class="s1">object</span><span class="dl">'</span><span class="p">)</span> <span class="p">{</span>
<span class="c1">// CommonJS module</span>
<span class="c1">// Load moment.js as an optional dependency</span>
<span class="c1">// try { moment = require('moment'); } catch (e) {}</span>
<span class="nx">module</span><span class="p">.</span><span class="nx">exports</span> <span class="o">=</span> <span class="nx">factory</span><span class="p">(</span><span class="nx">moment</span><span class="p">);</span>
<span class="p">}</span> <span class="k">else</span> <span class="k">if</span> <span class="p">(</span><span class="k">typeof</span> <span class="nx">define</span> <span class="o">===</span> <span class="dl">'</span><span class="s1">function</span><span class="dl">'</span> <span class="o">&&</span> <span class="nx">define</span><span class="p">.</span><span class="nx">amd</span><span class="p">)</span> <span class="p">{</span>
<span class="c1">// AMD. Register as an anonymous module.</span>
<span class="nx">define</span><span class="p">(</span><span class="kd">function</span> <span class="p">(</span><span class="nx">req</span><span class="p">)</span>
<span class="p">{</span>
<span class="c1">// Load moment.js as an optional dependency</span>
<span class="kd">var</span> <span class="nx">id</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">moment</span><span class="dl">'</span><span class="p">;</span>
<span class="c1">// try { moment = req(id); } catch (e) {}</span>
<span class="k">return</span> <span class="nx">factory</span><span class="p">(</span><span class="nx">moment</span><span class="p">);</span>
<span class="p">});</span>
<span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
<span class="nx">root</span><span class="p">.</span><span class="nx">Pikaday</span> <span class="o">=</span> <span class="nx">factory</span><span class="p">(</span><span class="nx">root</span><span class="p">.</span><span class="nx">moment</span><span class="p">);</span>
<span class="p">}</span>
<span class="p">}(</span><span class="k">this</span><span class="p">,</span> <span class="kd">function</span> <span class="p">(</span><span class="nx">moment</span><span class="p">)</span>
</code></pre></div></div>
<p>However, these issues are <a href="https://github.com/Pikaday/Pikaday/issues/815">on the roadmap to be fixed in Pikaday version 2.0</a> and messing around with code in the <code class="language-plaintext highlighter-rouge">node_modules</code> folder isn’t the best idea, so for now I’m going to ignore them.</p>
<h2 id="saving-an-event">Saving an Event</h2>
<p>To actually save an event to the database, we’re going to pass a callback function to our <code class="language-plaintext highlighter-rouge"><EventForm></code> component, that can be called in the context of its parent.</p>
<p>In the <code class="language-plaintext highlighter-rouge"><Editor></code> component:</p>
<div class="language-jsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">class</span> <span class="nx">Editor</span> <span class="kd">extends</span> <span class="nx">React</span><span class="p">.</span><span class="nx">Component</span> <span class="p">{</span>
<span class="kd">constructor</span><span class="p">(</span><span class="nx">props</span><span class="p">)</span> <span class="p">{</span>
<span class="p">...</span>
<span class="k">this</span><span class="p">.</span><span class="nx">addEvent</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">addEvent</span><span class="p">.</span><span class="nx">bind</span><span class="p">(</span><span class="k">this</span><span class="p">);</span>
<span class="p">}</span>
<span class="nx">componentDidMount</span><span class="p">()</span> <span class="p">{</span> <span class="p">...</span> <span class="p">}</span>
<span class="nx">addEvent</span><span class="p">(</span><span class="nx">newEvent</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">axios</span>
<span class="p">.</span><span class="nx">post</span><span class="p">(</span><span class="dl">'</span><span class="s1">/api/events.json</span><span class="dl">'</span><span class="p">,</span> <span class="nx">newEvent</span><span class="p">)</span>
<span class="p">.</span><span class="nx">then</span><span class="p">((</span><span class="nx">response</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">alert</span><span class="p">(</span><span class="dl">'</span><span class="s1">Event Added!</span><span class="dl">'</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">savedEvent</span> <span class="o">=</span> <span class="nx">response</span><span class="p">.</span><span class="nx">data</span><span class="p">;</span>
<span class="k">this</span><span class="p">.</span><span class="nx">setState</span><span class="p">(</span><span class="nx">prevState</span> <span class="o">=></span> <span class="p">({</span>
<span class="na">events</span><span class="p">:</span> <span class="p">[...</span><span class="nx">prevState</span><span class="p">.</span><span class="nx">events</span><span class="p">,</span> <span class="nx">savedEvent</span><span class="p">],</span>
<span class="p">}));</span>
<span class="kd">const</span> <span class="p">{</span> <span class="nx">history</span> <span class="p">}</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">props</span><span class="p">;</span>
<span class="nx">history</span><span class="p">.</span><span class="nx">push</span><span class="p">(</span><span class="s2">`/events/</span><span class="p">${</span><span class="nx">savedEvent</span><span class="p">.</span><span class="nx">id</span><span class="p">}</span><span class="s2">`</span><span class="p">);</span>
<span class="p">})</span>
<span class="p">.</span><span class="k">catch</span><span class="p">((</span><span class="nx">error</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">error</span><span class="p">);</span>
<span class="p">});</span>
<span class="p">}</span>
<span class="nx">render</span><span class="p">()</span> <span class="p">{</span>
<span class="p">...</span>
<span class="k">return</span> <span class="p">(</span>
<span class="p"><</span><span class="nt">div</span><span class="p">></span>
<span class="p"><</span><span class="nc">Header</span> <span class="p">/></span>
<span class="p"><</span><span class="nt">div</span> <span class="na">className</span><span class="p">=</span><span class="s">"grid"</span><span class="p">></span>
<span class="p"><</span><span class="nc">EventList</span> <span class="na">events</span><span class="p">=</span><span class="si">{</span><span class="nx">events</span><span class="si">}</span> <span class="na">activeId</span><span class="p">=</span><span class="si">{</span><span class="nb">Number</span><span class="p">(</span><span class="nx">eventId</span><span class="p">)</span><span class="si">}</span> <span class="p">/></span>
<span class="p"><</span><span class="nc">Switch</span><span class="p">></span>
<span class="p"><</span><span class="nc">PropsRoute</span> <span class="na">path</span><span class="p">=</span><span class="s">"/events/new"</span> <span class="na">component</span><span class="p">=</span><span class="si">{</span><span class="nx">EventForm</span><span class="si">}</span> <span class="na">onSubmit</span><span class="p">=</span><span class="si">{</span><span class="k">this</span><span class="p">.</span><span class="nx">addEvent</span><span class="si">}</span> <span class="p">/></span>
<span class="p"><</span><span class="nc">PropsRoute</span> <span class="na">path</span><span class="p">=</span><span class="s">"/events/:id"</span> <span class="na">component</span><span class="p">=</span><span class="si">{</span><span class="nx">Event</span><span class="si">}</span> <span class="na">event</span><span class="p">=</span><span class="si">{</span><span class="nx">event</span><span class="si">}</span> <span class="p">/></span>
<span class="p"></</span><span class="nc">Switch</span><span class="p">></span>
<span class="p"></</span><span class="nt">div</span><span class="p">></span>
<span class="p"></</span><span class="nt">div</span><span class="p">></span>
<span class="p">);</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="nx">Editor</span><span class="p">.</span><span class="nx">propTypes</span> <span class="o">=</span> <span class="p">{</span>
<span class="na">match</span><span class="p">:</span> <span class="nx">PropTypes</span><span class="p">.</span><span class="nx">shape</span><span class="p">(),</span>
<span class="na">history</span><span class="p">:</span> <span class="nx">PropTypes</span><span class="p">.</span><span class="nx">shape</span><span class="p">({</span> <span class="na">push</span><span class="p">:</span> <span class="nx">PropTypes</span><span class="p">.</span><span class="nx">func</span> <span class="p">}).</span><span class="nx">isRequired</span><span class="p">,</span>
<span class="p">};</span>
</code></pre></div></div>
<p>As you can see, we have defined an <code class="language-plaintext highlighter-rouge">addEvent</code> method, which receives a <code class="language-plaintext highlighter-rouge">newEvent</code> object and then fires off a request to our API to create a new event using that data. If the request is successful, it will add the newly created event to the array of events that are being held in state and the UI will update accordingly. It will also use the <code class="language-plaintext highlighter-rouge">history</code> object, which is made available to us by React Router, to change the URL to that of the newly created event.</p>
<p>Note also that we are passing the <code class="language-plaintext highlighter-rouge">addEvent</code> method into the <code class="language-plaintext highlighter-rouge">EventForm</code> component as a callback. Now, all we’ve got to do is call it in our <code class="language-plaintext highlighter-rouge"><EventForm></code> component:</p>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">handleSubmit</span><span class="p">(</span><span class="nx">e</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">e</span><span class="p">.</span><span class="nx">preventDefault</span><span class="p">();</span>
<span class="kd">const</span> <span class="p">{</span> <span class="nx">event</span> <span class="p">}</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">state</span><span class="p">;</span>
<span class="kd">const</span> <span class="nx">errors</span> <span class="o">=</span> <span class="nx">validateEvent</span><span class="p">(</span><span class="nx">event</span><span class="p">);</span>
<span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">isEmptyObject</span><span class="p">(</span><span class="nx">errors</span><span class="p">))</span> <span class="p">{</span>
<span class="k">this</span><span class="p">.</span><span class="nx">setState</span><span class="p">({</span> <span class="nx">errors</span> <span class="p">});</span>
<span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
<span class="kd">const</span> <span class="p">{</span> <span class="nx">onSubmit</span> <span class="p">}</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">props</span><span class="p">;</span>
<span class="nx">onSubmit</span><span class="p">(</span><span class="nx">event</span><span class="p">);</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="p">...</span>
<span class="nx">EventForm</span><span class="p">.</span><span class="nx">propTypes</span> <span class="o">=</span> <span class="p">{</span>
<span class="na">event</span><span class="p">:</span> <span class="nx">PropTypes</span><span class="p">.</span><span class="nx">shape</span><span class="p">(),</span>
<span class="na">onSubmit</span><span class="p">:</span> <span class="nx">PropTypes</span><span class="p">.</span><span class="nx">func</span><span class="p">.</span><span class="nx">isRequired</span><span class="p">,</span>
<span class="p">};</span>
</code></pre></div></div>
<p>Now, when you attempt to save an event to the database, you should get an alert pop up, informing you that the save was successful. If you’re following along, I’d encourage you to give this a try and satisfy yourself that everything is working before continuing.</p>
<h2 id="deleting-events">Deleting Events</h2>
<p>Now, if you’re anything like me, you will have created a bunch of silly events while following along with this tutorial. Let’s add a delete button so that we can nuke them.</p>
<p>As with adding an event, we’ll want to declare a method to delete an event in our <code class="language-plaintext highlighter-rouge"><Editor></code> component and pass it to our <code class="language-plaintext highlighter-rouge"><Event></code> component as a prop.</p>
<p>First the method:</p>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">class</span> <span class="nx">Editor</span> <span class="kd">extends</span> <span class="nx">React</span><span class="p">.</span><span class="nx">Component</span> <span class="p">{</span>
<span class="kd">constructor</span><span class="p">(</span><span class="nx">props</span><span class="p">)</span> <span class="p">{</span>
<span class="p">...</span>
<span class="k">this</span><span class="p">.</span><span class="nx">deleteEvent</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">deleteEvent</span><span class="p">.</span><span class="nx">bind</span><span class="p">(</span><span class="k">this</span><span class="p">);</span>
<span class="p">}</span>
<span class="p">...</span>
<span class="nx">deleteEvent</span><span class="p">(</span><span class="nx">eventId</span><span class="p">)</span> <span class="p">{</span>
<span class="kd">const</span> <span class="nx">sure</span> <span class="o">=</span> <span class="nb">window</span><span class="p">.</span><span class="nx">confirm</span><span class="p">(</span><span class="dl">'</span><span class="s1">Are you sure?</span><span class="dl">'</span><span class="p">);</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">sure</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">axios</span>
<span class="p">.</span><span class="k">delete</span><span class="p">(</span><span class="s2">`/api/events/</span><span class="p">${</span><span class="nx">eventId</span><span class="p">}</span><span class="s2">.json`</span><span class="p">)</span>
<span class="p">.</span><span class="nx">then</span><span class="p">((</span><span class="nx">response</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">response</span><span class="p">.</span><span class="nx">status</span> <span class="o">===</span> <span class="mi">204</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">alert</span><span class="p">(</span><span class="dl">'</span><span class="s1">Event deleted</span><span class="dl">'</span><span class="p">);</span>
<span class="kd">const</span> <span class="p">{</span> <span class="nx">history</span> <span class="p">}</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">props</span><span class="p">;</span>
<span class="nx">history</span><span class="p">.</span><span class="nx">push</span><span class="p">(</span><span class="dl">'</span><span class="s1">/events</span><span class="dl">'</span><span class="p">);</span>
<span class="kd">const</span> <span class="p">{</span> <span class="nx">events</span> <span class="p">}</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">state</span><span class="p">;</span>
<span class="k">this</span><span class="p">.</span><span class="nx">setState</span><span class="p">({</span> <span class="na">events</span><span class="p">:</span> <span class="nx">events</span><span class="p">.</span><span class="nx">filter</span><span class="p">(</span><span class="nx">event</span> <span class="o">=></span> <span class="nx">event</span><span class="p">.</span><span class="nx">id</span> <span class="o">!==</span> <span class="nx">eventId</span><span class="p">)</span> <span class="p">});</span>
<span class="p">}</span>
<span class="p">})</span>
<span class="p">.</span><span class="k">catch</span><span class="p">((</span><span class="nx">error</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">error</span><span class="p">);</span>
<span class="p">});</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<p>In out <code class="language-plaintext highlighter-rouge">deleteEvent</code> method, we ask the user for confirmation that they really want to delete the event via a confirm dialogue. If the user is sure, we send a DELETE request to our API and once a successful response comes back, we inform the user that the event has been deleted, redirect the user to <code class="language-plaintext highlighter-rouge">/events</code> and remove the deleted event from state. As with the <code class="language-plaintext highlighter-rouge">addEvent</code> method, if the response from the API is anything other than success, we log the error to the console.</p>
<p>Next, pass the <code class="language-plaintext highlighter-rouge">deleteEvent</code> callback to the <code class="language-plaintext highlighter-rouge"><Event></code> component:</p>
<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt"><PropsRoute</span>
<span class="na">path=</span><span class="s">"/events/:id"</span>
<span class="na">component=</span><span class="s">{Event}</span>
<span class="na">event=</span><span class="s">{event}</span>
<span class="na">onDelete=</span><span class="s">{this.deleteEvent}</span>
<span class="nt">/></span>
</code></pre></div></div>
<p>Now, in the <code class="language-plaintext highlighter-rouge"><Event></code> component we can create a button to delete the event:</p>
<div class="language-jsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">Event</span> <span class="o">=</span> <span class="p">({</span> <span class="nx">event</span><span class="p">,</span> <span class="nx">onDelete</span> <span class="p">})</span> <span class="o">=></span> <span class="p">(</span>
<span class="p"><</span><span class="nt">div</span> <span class="na">className</span><span class="p">=</span><span class="s">"eventContainer"</span><span class="p">></span>
<span class="p"><</span><span class="nt">h2</span><span class="p">></span>
<span class="si">{</span><span class="nx">event</span><span class="p">.</span><span class="nx">event_date</span><span class="si">}</span>
<span class="si">{</span><span class="dl">'</span><span class="s1"> - </span><span class="dl">'</span><span class="si">}</span>
<span class="si">{</span><span class="nx">event</span><span class="p">.</span><span class="nx">event_type</span><span class="si">}</span>
<span class="si">{</span><span class="dl">'</span><span class="s1"> </span><span class="dl">'</span><span class="si">}</span>
<span class="p"><</span><span class="nt">button</span> <span class="na">className</span><span class="p">=</span><span class="s">"delete"</span> <span class="na">type</span><span class="p">=</span><span class="s">"button"</span> <span class="na">onClick</span><span class="p">=</span><span class="si">{</span><span class="p">()</span> <span class="o">=></span> <span class="nx">onDelete</span><span class="p">(</span><span class="nx">event</span><span class="p">.</span><span class="nx">id</span><span class="p">)</span><span class="si">}</span><span class="p">></span>
Delete
<span class="p"></</span><span class="nt">button</span><span class="p">></span>
<span class="p"></</span><span class="nt">h2</span><span class="p">></span>
...
<span class="p"></</span><span class="nt">div</span><span class="p">></span>
<span class="p">);</span>
<span class="nx">Event</span><span class="p">.</span><span class="nx">propTypes</span> <span class="o">=</span> <span class="p">{</span>
<span class="na">event</span><span class="p">:</span> <span class="nx">PropTypes</span><span class="p">.</span><span class="nx">shape</span><span class="p">(),</span>
<span class="na">onDelete</span><span class="p">:</span> <span class="nx">PropTypes</span><span class="p">.</span><span class="nx">func</span><span class="p">.</span><span class="nx">isRequired</span><span class="p">,</span>
<span class="p">};</span>
</code></pre></div></div>
<p>And now we can delete events.</p>
<h2 id="adding-flash-messages">Adding Flash Messages</h2>
<p>Alerts are all well and good to tell the user that something happened, but they don’t look very pretty. Let’s add flash message functionality instead, using the <a href="https://www.npmjs.com/package/react-s-alert">react-s-alert library</a>.</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>yarn add react-s-alert
</code></pre></div></div>
<p>We’ll stick this functionality in its own helper file, <code class="language-plaintext highlighter-rouge">notifications.js</code>.</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">touch </span>app/javascript/helpers/notifications.js
</code></pre></div></div>
<p>Then add:</p>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="nx">Alert</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">react-s-alert</span><span class="dl">'</span><span class="p">;</span>
<span class="k">import</span> <span class="dl">'</span><span class="s1">react-s-alert/dist/s-alert-default.css</span><span class="dl">'</span><span class="p">;</span>
<span class="k">import</span> <span class="dl">'</span><span class="s1">react-s-alert/dist/s-alert-css-effects/scale.css</span><span class="dl">'</span><span class="p">;</span>
<span class="c1">// Uncomment as needed</span>
<span class="c1">// import 'react-s-alert/dist/s-alert-css-effects/slide.css';</span>
<span class="c1">// import 'react-s-alert/dist/s-alert-css-effects/bouncyflip.css';</span>
<span class="c1">// import 'react-s-alert/dist/s-alert-css-effects/flip.css';</span>
<span class="c1">// import 'react-s-alert/dist/s-alert-css-effects/genie.css';</span>
<span class="c1">// import 'react-s-alert/dist/s-alert-css-effects/jelly.css';</span>
<span class="c1">// import 'react-s-alert/dist/s-alert-css-effects/stackslide.css';</span>
<span class="kd">const</span> <span class="nx">defaults</span> <span class="o">=</span> <span class="p">{</span>
<span class="na">position</span><span class="p">:</span> <span class="dl">'</span><span class="s1">top-right</span><span class="dl">'</span><span class="p">,</span>
<span class="na">effect</span><span class="p">:</span> <span class="dl">'</span><span class="s1">scale</span><span class="dl">'</span><span class="p">,</span>
<span class="na">timeout</span><span class="p">:</span> <span class="mi">3500</span><span class="p">,</span>
<span class="na">offset</span><span class="p">:</span> <span class="mi">45</span><span class="p">,</span>
<span class="p">};</span>
<span class="k">export</span> <span class="p">{</span> <span class="nx">Alert</span> <span class="p">};</span>
<span class="k">export</span> <span class="kd">const</span> <span class="nx">success</span> <span class="o">=</span> <span class="p">(</span><span class="nx">message</span><span class="p">,</span> <span class="nx">options</span> <span class="o">=</span> <span class="p">{})</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">Alert</span><span class="p">.</span><span class="nx">success</span><span class="p">(</span><span class="nx">message</span><span class="p">,</span> <span class="nb">Object</span><span class="p">.</span><span class="nx">assign</span><span class="p">(</span><span class="nx">defaults</span><span class="p">,</span> <span class="nx">options</span><span class="p">));</span>
<span class="p">};</span>
<span class="k">export</span> <span class="kd">const</span> <span class="nx">info</span> <span class="o">=</span> <span class="p">(</span><span class="nx">message</span><span class="p">,</span> <span class="nx">options</span> <span class="o">=</span> <span class="p">{})</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">Alert</span><span class="p">.</span><span class="nx">info</span><span class="p">(</span><span class="nx">message</span><span class="p">,</span> <span class="nb">Object</span><span class="p">.</span><span class="nx">assign</span><span class="p">(</span><span class="nx">defaults</span><span class="p">,</span> <span class="nx">options</span><span class="p">));</span>
<span class="p">};</span>
<span class="k">export</span> <span class="kd">const</span> <span class="nx">warning</span> <span class="o">=</span> <span class="p">(</span><span class="nx">message</span><span class="p">,</span> <span class="nx">options</span> <span class="o">=</span> <span class="p">{})</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">Alert</span><span class="p">.</span><span class="nx">warning</span><span class="p">(</span><span class="nx">message</span><span class="p">,</span> <span class="nb">Object</span><span class="p">.</span><span class="nx">assign</span><span class="p">(</span><span class="nx">defaults</span><span class="p">,</span> <span class="nx">options</span><span class="p">));</span>
<span class="p">};</span>
<span class="k">export</span> <span class="kd">const</span> <span class="nx">error</span> <span class="o">=</span> <span class="p">(</span><span class="nx">message</span><span class="p">,</span> <span class="nx">options</span> <span class="o">=</span> <span class="p">{})</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">Alert</span><span class="p">.</span><span class="nx">error</span><span class="p">(</span><span class="nx">message</span><span class="p">,</span> <span class="nb">Object</span><span class="p">.</span><span class="nx">assign</span><span class="p">(</span><span class="nx">defaults</span><span class="p">,</span> <span class="nx">options</span><span class="p">));</span>
<span class="p">};</span>
</code></pre></div></div>
<p>Now we have a centralized place to set some sensible defaults and can reduce the boilerplate when calling the flash messages. You’ll also notice that I’m including the scale effect to animate the display of the messages. Note that there are a whole bunch of other effects which can be tried out by uncommenting the appropriate line and altering the default options accordingly.</p>
<p>Next, include the library in the <code class="language-plaintext highlighter-rouge"><App></code> component:</p>
<div class="language-jsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="nx">React</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">react</span><span class="dl">'</span><span class="p">;</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">Route</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">react-router-dom</span><span class="dl">'</span><span class="p">;</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">Alert</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">../helpers/notifications</span><span class="dl">'</span><span class="p">;</span>
<span class="k">import</span> <span class="nx">Editor</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">./Editor</span><span class="dl">'</span><span class="p">;</span>
<span class="k">import</span> <span class="dl">'</span><span class="s1">./App.css</span><span class="dl">'</span><span class="p">;</span>
<span class="kd">const</span> <span class="nx">App</span> <span class="o">=</span> <span class="p">()</span> <span class="o">=></span> <span class="p">(</span>
<span class="p"><</span><span class="nt">div</span><span class="p">></span>
<span class="p"><</span><span class="nc">Route</span> <span class="na">path</span><span class="p">=</span><span class="s">"/events/:id?"</span> <span class="na">component</span><span class="p">=</span><span class="si">{</span><span class="nx">Editor</span><span class="si">}</span> <span class="p">/></span>
<span class="p"><</span><span class="nc">Alert</span> <span class="na">stack</span><span class="p">=</span><span class="si">{</span> <span class="p">{</span> <span class="na">limit</span><span class="p">:</span> <span class="mi">3</span> <span class="p">}</span> <span class="si">}</span> <span class="p">/></span>
<span class="p"></</span><span class="nt">div</span><span class="p">></span>
<span class="p">);</span>
<span class="k">export</span> <span class="k">default</span> <span class="nx">App</span><span class="p">;</span>
</code></pre></div></div>
<p>And use it in the <code class="language-plaintext highlighter-rouge"><Editor></code> component to replace our alerts:</p>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">success</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">../helpers/notifications</span><span class="dl">'</span><span class="p">;</span>
<span class="p">...</span>
<span class="nx">addEvent</span><span class="p">(</span><span class="nx">newEvent</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">axios</span>
<span class="p">.</span><span class="nx">post</span><span class="p">(</span><span class="dl">'</span><span class="s1">/api/events.json</span><span class="dl">'</span><span class="p">,</span> <span class="nx">newEvent</span><span class="p">)</span>
<span class="p">.</span><span class="nx">then</span><span class="p">((</span><span class="nx">response</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">success</span><span class="p">(</span><span class="dl">'</span><span class="s1">Event Added!</span><span class="dl">'</span><span class="p">);</span>
<span class="p">...</span>
<span class="p">}</span>
<span class="nx">deleteEvent</span><span class="p">(</span><span class="nx">eventId</span><span class="p">)</span> <span class="p">{</span>
<span class="kd">const</span> <span class="nx">sure</span> <span class="o">=</span> <span class="nb">window</span><span class="p">.</span><span class="nx">confirm</span><span class="p">(</span><span class="dl">'</span><span class="s1">Are you sure?</span><span class="dl">'</span><span class="p">);</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">sure</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">axios</span>
<span class="p">.</span><span class="k">delete</span><span class="p">(</span><span class="s2">`/api/events/</span><span class="p">${</span><span class="nx">eventId</span><span class="p">}</span><span class="s2">.json`</span><span class="p">)</span>
<span class="p">.</span><span class="nx">then</span><span class="p">((</span><span class="nx">response</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">response</span><span class="p">.</span><span class="nx">status</span> <span class="o">===</span> <span class="mi">204</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">success</span><span class="p">(</span><span class="dl">'</span><span class="s1">Event deleted</span><span class="dl">'</span><span class="p">);</span>
<span class="p">...</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<p>While we’re at it we can move the error handling into a helper method, too. In <code class="language-plaintext highlighter-rouge">app/javascript/helpers/helpers.js</code>:</p>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">error</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">./notifications</span><span class="dl">'</span><span class="p">;</span>
<span class="p">...</span>
<span class="k">export</span> <span class="kd">const</span> <span class="nx">handleAjaxError</span> <span class="o">=</span> <span class="p">(</span><span class="nx">err</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">error</span><span class="p">(</span><span class="dl">'</span><span class="s1">Something went wrong</span><span class="dl">'</span><span class="p">);</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">warn</span><span class="p">(</span><span class="nx">err</span><span class="p">);</span>
<span class="p">};</span>
</code></pre></div></div>
<p>And in the <code class="language-plaintext highlighter-rouge"><Editor></code> component:</p>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">handleAjaxError</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">../helpers/helpers</span><span class="dl">'</span><span class="p">;</span>
</code></pre></div></div>
<p>And replace the three occurences of:</p>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">.</span><span class="k">catch</span><span class="p">((</span><span class="nx">error</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">error</span><span class="p">);</span>
<span class="p">});</span>
</code></pre></div></div>
<p>With:</p>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">.</span><span class="k">catch</span><span class="p">(</span><span class="nx">handleAjaxError</span><span class="p">);</span>
</code></pre></div></div>
<p>Now, when you create or delete an event, you should get a nicely styled flash message.</p>
<p><img src="https://res.cloudinary.com/hibbard/image/upload/v1549014887/event-manager/event-manager-06.png" alt="Event Manager - Flash message" /></p>
<h2 id="updating-an-event">Updating an Event</h2>
<p>The final piece of our CRUD functionality to add is the ability to update an event. Remember when we declared an event as props in our <code class="language-plaintext highlighter-rouge"><EventForm></code> component? Well, this enables us to re-use the same form to update an event — if we pass an event into the component as props, the event details should pre-populate the form, otherwise the component falls back to its sensible defaults, which are exactly what we need to create a new event.</p>
<p>Let’s start by adding the <em>Edit</em> link to the <code class="language-plaintext highlighter-rouge"><Event></code> component. It’s fine to make this a link, as it will change the URL:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>import { Link } from 'react-router-dom';
...
<h2>
{event.event_date}
{' - '}
{event.event_type}
{' '}
<Link to={`/events/${event.id}/edit`}>Edit</Link>
<button className="delete" type="button" onClick={() => onDelete(event.id)}>
Delete
</button>
</h2>
</code></pre></div></div>
<p>In the <code class="language-plaintext highlighter-rouge"><Editor></code> component, let’s add the <code class="language-plaintext highlighter-rouge">updateEvent</code> method, bind it to the component instance and pass it as a prop to the <code class="language-plaintext highlighter-rouge"><EventForm></code> component:</p>
<div class="language-jsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">class</span> <span class="nx">Editor</span> <span class="kd">extends</span> <span class="nx">React</span><span class="p">.</span><span class="nx">Component</span> <span class="p">{</span>
<span class="kd">constructor</span><span class="p">(</span><span class="nx">props</span><span class="p">)</span> <span class="p">{</span>
<span class="p">...</span>
<span class="k">this</span><span class="p">.</span><span class="nx">updateEvent</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">updateEvent</span><span class="p">.</span><span class="nx">bind</span><span class="p">(</span><span class="k">this</span><span class="p">);</span>
<span class="p">}</span>
<span class="p">...</span>
<span class="nx">updateEvent</span><span class="p">(</span><span class="nx">updatedEvent</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">axios</span>
<span class="p">.</span><span class="nx">put</span><span class="p">(</span><span class="s2">`/api/events/</span><span class="p">${</span><span class="nx">updatedEvent</span><span class="p">.</span><span class="nx">id</span><span class="p">}</span><span class="s2">.json`</span><span class="p">,</span> <span class="nx">updatedEvent</span><span class="p">)</span>
<span class="p">.</span><span class="nx">then</span><span class="p">(()</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">success</span><span class="p">(</span><span class="dl">'</span><span class="s1">Event updated</span><span class="dl">'</span><span class="p">);</span>
<span class="kd">const</span> <span class="p">{</span> <span class="nx">events</span> <span class="p">}</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">state</span><span class="p">;</span>
<span class="kd">const</span> <span class="nx">idx</span> <span class="o">=</span> <span class="nx">events</span><span class="p">.</span><span class="nx">findIndex</span><span class="p">(</span><span class="nx">event</span> <span class="o">=></span> <span class="nx">event</span><span class="p">.</span><span class="nx">id</span> <span class="o">===</span> <span class="nx">updatedEvent</span><span class="p">.</span><span class="nx">id</span><span class="p">);</span>
<span class="nx">events</span><span class="p">[</span><span class="nx">idx</span><span class="p">]</span> <span class="o">=</span> <span class="nx">updatedEvent</span><span class="p">;</span>
<span class="kd">const</span> <span class="p">{</span> <span class="nx">history</span> <span class="p">}</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">props</span><span class="p">;</span>
<span class="nx">history</span><span class="p">.</span><span class="nx">push</span><span class="p">(</span><span class="s2">`/events/</span><span class="p">${</span><span class="nx">updatedEvent</span><span class="p">.</span><span class="nx">id</span><span class="p">}</span><span class="s2">`</span><span class="p">);</span>
<span class="k">this</span><span class="p">.</span><span class="nx">setState</span><span class="p">({</span> <span class="nx">events</span> <span class="p">});</span>
<span class="p">})</span>
<span class="p">.</span><span class="k">catch</span><span class="p">(</span><span class="nx">handleAjaxError</span><span class="p">);</span>
<span class="p">}</span>
<span class="p">...</span>
<span class="nx">render</span><span class="p">()</span> <span class="p">{</span>
<span class="p">...</span>
<span class="k">return</span> <span class="p">(</span>
<span class="p"><</span><span class="nt">div</span><span class="p">></span>
...
<span class="p"><</span><span class="nc">Switch</span><span class="p">></span>
<span class="p"><</span><span class="nc">PropsRoute</span> <span class="na">path</span><span class="p">=</span><span class="s">"/events/new"</span> <span class="na">component</span><span class="p">=</span><span class="si">{</span><span class="nx">EventForm</span><span class="si">}</span> <span class="na">onSubmit</span><span class="p">=</span><span class="si">{</span><span class="k">this</span><span class="p">.</span><span class="nx">addEvent</span><span class="si">}</span> <span class="p">/></span>
<span class="p"><</span><span class="nc">PropsRoute</span>
<span class="na">exact</span>
<span class="na">path</span><span class="p">=</span><span class="s">"/events/:id/edit"</span>
<span class="na">component</span><span class="p">=</span><span class="si">{</span><span class="nx">EventForm</span><span class="si">}</span>
<span class="na">event</span><span class="p">=</span><span class="si">{</span><span class="nx">event</span><span class="si">}</span>
<span class="na">onSubmit</span><span class="p">=</span><span class="si">{</span><span class="k">this</span><span class="p">.</span><span class="nx">updateEvent</span><span class="si">}</span>
<span class="p">/></span>
<span class="p"><</span><span class="nc">PropsRoute</span>
<span class="na">path</span><span class="p">=</span><span class="s">"/events/:id"</span>
<span class="na">component</span><span class="p">=</span><span class="si">{</span><span class="nx">Event</span><span class="si">}</span>
<span class="na">event</span><span class="p">=</span><span class="si">{</span><span class="nx">event</span><span class="si">}</span>
<span class="na">onDelete</span><span class="p">=</span><span class="si">{</span><span class="k">this</span><span class="p">.</span><span class="nx">deleteEvent</span><span class="si">}</span>
<span class="p">/></span>
<span class="p"></</span><span class="nc">Switch</span><span class="p">></span>
...
<span class="p"></</span><span class="nt">div</span><span class="p">></span>
<span class="p">);</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<p>Note that the route order is important, as otherwise <code class="language-plaintext highlighter-rouge">path="/events/:id"</code> will match first and the form won’t be displayed.</p>
<p>Finally, in the <code class="language-plaintext highlighter-rouge"><EventForm></code> component, we need to pull the event out of state and set the values in the form accordingly.</p>
<div class="language-jsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">render</span><span class="p">()</span> <span class="p">{</span>
<span class="kd">const</span> <span class="p">{</span> <span class="nx">event</span> <span class="p">}</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">state</span><span class="p">;</span>
<span class="k">return</span> <span class="p">(</span>
<span class="p"><</span><span class="nt">div</span><span class="p">></span>
<span class="p"><</span><span class="nt">h2</span><span class="p">></span>New Event<span class="p"></</span><span class="nt">h2</span><span class="p">></span>
<span class="si">{</span><span class="k">this</span><span class="p">.</span><span class="nx">renderErrors</span><span class="p">()</span><span class="si">}</span>
<span class="p"><</span><span class="nt">form</span> <span class="na">className</span><span class="p">=</span><span class="s">"eventForm"</span> <span class="na">onSubmit</span><span class="p">=</span><span class="si">{</span><span class="k">this</span><span class="p">.</span><span class="nx">handleSubmit</span><span class="si">}</span><span class="p">></span>
<span class="p"><</span><span class="nt">div</span><span class="p">></span>
<span class="p"><</span><span class="nt">label</span> <span class="na">htmlFor</span><span class="p">=</span><span class="s">"event_type"</span><span class="p">></span>
<span class="p"><</span><span class="nt">strong</span><span class="p">></span>Type:<span class="p"></</span><span class="nt">strong</span><span class="p">></span>
<span class="p"><</span><span class="nt">input</span>
<span class="na">type</span><span class="p">=</span><span class="s">"text"</span>
<span class="na">id</span><span class="p">=</span><span class="s">"event_type"</span>
<span class="na">name</span><span class="p">=</span><span class="s">"event_type"</span>
<span class="na">onChange</span><span class="p">=</span><span class="si">{</span><span class="k">this</span><span class="p">.</span><span class="nx">handleInputChange</span><span class="si">}</span>
<span class="na">value</span><span class="p">=</span><span class="si">{</span><span class="nx">event</span><span class="p">.</span><span class="nx">event_type</span><span class="si">}</span>
<span class="p">/></span>
<span class="p"></</span><span class="nt">label</span><span class="p">></span>
<span class="p"></</span><span class="nt">div</span><span class="p">></span>
<span class="p"><</span><span class="nt">div</span><span class="p">></span>
<span class="p"><</span><span class="nt">label</span> <span class="na">htmlFor</span><span class="p">=</span><span class="s">"event_date"</span><span class="p">></span>
<span class="p"><</span><span class="nt">strong</span><span class="p">></span>Date:<span class="p"></</span><span class="nt">strong</span><span class="p">></span>
<span class="p"><</span><span class="nt">input</span>
<span class="na">type</span><span class="p">=</span><span class="s">"text"</span>
<span class="na">id</span><span class="p">=</span><span class="s">"event_date"</span>
<span class="na">name</span><span class="p">=</span><span class="s">"event_date"</span>
<span class="na">ref</span><span class="p">=</span><span class="si">{</span><span class="k">this</span><span class="p">.</span><span class="nx">dateInput</span><span class="si">}</span>
<span class="na">autoComplete</span><span class="p">=</span><span class="s">"off"</span>
<span class="na">value</span><span class="p">=</span><span class="si">{</span><span class="nx">event</span><span class="p">.</span><span class="nx">event_date</span><span class="si">}</span>
<span class="na">onChange</span><span class="p">=</span><span class="si">{</span><span class="k">this</span><span class="p">.</span><span class="nx">handleInputChange</span><span class="si">}</span>
<span class="p">/></span>
<span class="p"></</span><span class="nt">label</span><span class="p">></span>
<span class="p"></</span><span class="nt">div</span><span class="p">></span>
<span class="p"><</span><span class="nt">div</span><span class="p">></span>
<span class="p"><</span><span class="nt">label</span> <span class="na">htmlFor</span><span class="p">=</span><span class="s">"title"</span><span class="p">></span>
<span class="p"><</span><span class="nt">strong</span><span class="p">></span>Title:<span class="p"></</span><span class="nt">strong</span><span class="p">></span>
<span class="p"><</span><span class="nt">textarea</span>
<span class="na">cols</span><span class="p">=</span><span class="s">"30"</span>
<span class="na">rows</span><span class="p">=</span><span class="s">"10"</span>
<span class="na">id</span><span class="p">=</span><span class="s">"title"</span>
<span class="na">name</span><span class="p">=</span><span class="s">"title"</span>
<span class="na">onChange</span><span class="p">=</span><span class="si">{</span><span class="k">this</span><span class="p">.</span><span class="nx">handleInputChange</span><span class="si">}</span>
<span class="na">value</span><span class="p">=</span><span class="si">{</span><span class="nx">event</span><span class="p">.</span><span class="nx">title</span><span class="si">}</span>
<span class="p">/></span>
<span class="p"></</span><span class="nt">label</span><span class="p">></span>
<span class="p"></</span><span class="nt">div</span><span class="p">></span>
<span class="p"><</span><span class="nt">div</span><span class="p">></span>
<span class="p"><</span><span class="nt">label</span> <span class="na">htmlFor</span><span class="p">=</span><span class="s">"speaker"</span><span class="p">></span>
<span class="p"><</span><span class="nt">strong</span><span class="p">></span>Speakers:<span class="p"></</span><span class="nt">strong</span><span class="p">></span>
<span class="p"><</span><span class="nt">input</span>
<span class="na">type</span><span class="p">=</span><span class="s">"text"</span>
<span class="na">id</span><span class="p">=</span><span class="s">"speaker"</span>
<span class="na">name</span><span class="p">=</span><span class="s">"speaker"</span>
<span class="na">onChange</span><span class="p">=</span><span class="si">{</span><span class="k">this</span><span class="p">.</span><span class="nx">handleInputChange</span><span class="si">}</span>
<span class="na">value</span><span class="p">=</span><span class="si">{</span><span class="nx">event</span><span class="p">.</span><span class="nx">speaker</span><span class="si">}</span>
<span class="p">/></span>
<span class="p"></</span><span class="nt">label</span><span class="p">></span>
<span class="p"></</span><span class="nt">div</span><span class="p">></span>
<span class="p"><</span><span class="nt">div</span><span class="p">></span>
<span class="p"><</span><span class="nt">label</span> <span class="na">htmlFor</span><span class="p">=</span><span class="s">"host"</span><span class="p">></span>
<span class="p"><</span><span class="nt">strong</span><span class="p">></span>Hosts:<span class="p"></</span><span class="nt">strong</span><span class="p">></span>
<span class="p"><</span><span class="nt">input</span>
<span class="na">type</span><span class="p">=</span><span class="s">"text"</span>
<span class="na">id</span><span class="p">=</span><span class="s">"host"</span>
<span class="na">name</span><span class="p">=</span><span class="s">"host"</span>
<span class="na">onChange</span><span class="p">=</span><span class="si">{</span><span class="k">this</span><span class="p">.</span><span class="nx">handleInputChange</span><span class="si">}</span>
<span class="na">value</span><span class="p">=</span><span class="si">{</span><span class="nx">event</span><span class="p">.</span><span class="nx">host</span><span class="si">}</span>
<span class="p">/></span>
<span class="p"></</span><span class="nt">label</span><span class="p">></span>
<span class="p"></</span><span class="nt">div</span><span class="p">></span>
<span class="p"><</span><span class="nt">div</span><span class="p">></span>
<span class="p"><</span><span class="nt">label</span> <span class="na">htmlFor</span><span class="p">=</span><span class="s">"published"</span><span class="p">></span>
<span class="p"><</span><span class="nt">strong</span><span class="p">></span>Publish:<span class="p"></</span><span class="nt">strong</span><span class="p">></span>
<span class="p"><</span><span class="nt">input</span>
<span class="na">type</span><span class="p">=</span><span class="s">"checkbox"</span>
<span class="na">id</span><span class="p">=</span><span class="s">"published"</span>
<span class="na">name</span><span class="p">=</span><span class="s">"published"</span>
<span class="na">onChange</span><span class="p">=</span><span class="si">{</span><span class="k">this</span><span class="p">.</span><span class="nx">handleInputChange</span><span class="si">}</span>
<span class="na">checked</span><span class="p">=</span><span class="si">{</span><span class="nx">event</span><span class="p">.</span><span class="nx">published</span><span class="si">}</span>
<span class="p">/></span>
<span class="p"></</span><span class="nt">label</span><span class="p">></span>
<span class="p"></</span><span class="nt">div</span><span class="p">></span>
<span class="p"><</span><span class="nt">div</span> <span class="na">className</span><span class="p">=</span><span class="s">"form-actions"</span><span class="p">></span>
<span class="p"><</span><span class="nt">button</span> <span class="na">type</span><span class="p">=</span><span class="s">"submit"</span><span class="p">></span>Save<span class="p"></</span><span class="nt">button</span><span class="p">></span>
<span class="p"></</span><span class="nt">div</span><span class="p">></span>
<span class="p"></</span><span class="nt">form</span><span class="p">></span>
<span class="p"></</span><span class="nt">div</span><span class="p">></span>
<span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>
<p>We also need to pass the datepicker a <code class="language-plaintext highlighter-rouge">toString</code> function in its initial configuration, so that the date is formatted properly when the event date is added to the form:</p>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">componentDidMount</span><span class="p">()</span> <span class="p">{</span>
<span class="k">new</span> <span class="nx">Pikaday</span><span class="p">({</span>
<span class="na">field</span><span class="p">:</span> <span class="k">this</span><span class="p">.</span><span class="nx">dateInput</span><span class="p">.</span><span class="nx">current</span><span class="p">,</span>
<span class="na">toString</span><span class="p">:</span> <span class="nx">date</span> <span class="o">=></span> <span class="nx">formatDate</span><span class="p">(</span><span class="nx">date</span><span class="p">),</span>
<span class="na">onSelect</span><span class="p">:</span> <span class="p">(</span><span class="nx">date</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="kd">const</span> <span class="nx">formattedDate</span> <span class="o">=</span> <span class="nx">formatDate</span><span class="p">(</span><span class="nx">date</span><span class="p">);</span>
<span class="k">this</span><span class="p">.</span><span class="nx">dateInput</span><span class="p">.</span><span class="nx">current</span><span class="p">.</span><span class="nx">value</span> <span class="o">=</span> <span class="nx">formattedDate</span><span class="p">;</span>
<span class="k">this</span><span class="p">.</span><span class="nx">updateEvent</span><span class="p">(</span><span class="dl">'</span><span class="s1">event_date</span><span class="dl">'</span><span class="p">,</span> <span class="nx">formattedDate</span><span class="p">);</span>
<span class="p">},</span>
<span class="p">});</span>
<span class="p">}</span>
</code></pre></div></div>
<p>And finally, we need to hook into the <code class="language-plaintext highlighter-rouge">componentWillReceiveProps</code> lifecycle method to ensure that the fields are cleared when a user is editing an event, then clicks <em>New Event</em>.</p>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">componentWillReceiveProps</span><span class="p">({</span> <span class="nx">event</span> <span class="p">})</span> <span class="p">{</span>
<span class="k">this</span><span class="p">.</span><span class="nx">setState</span><span class="p">({</span> <span class="nx">event</span> <span class="p">});</span>
<span class="p">}</span>
</code></pre></div></div>
<p>And that’s that. We now have all of our CRUD functionality.</p>
<h2 id="adding-some-form-tweaks">Adding Some Form Tweaks</h2>
<p>Next, let’s add a <em>Cancel</em> button to the form (in case the user changes their mind whilst editing or creating an event). We’ll also change the title of the form to reflect which action they are performing. And while we’re at it, we’ll improve the validation for our date field — at the moment it just checks if the user has entered a value.</p>
<p>In the <code class="language-plaintext highlighter-rouge"><EventForm></code> component:</p>
<div class="language-jsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">Link</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">react-router-dom</span><span class="dl">'</span><span class="p">;</span>
<span class="nx">render</span><span class="p">()</span> <span class="p">{</span>
<span class="kd">const</span> <span class="p">{</span> <span class="nx">event</span> <span class="p">}</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">state</span><span class="p">;</span>
<span class="kd">const</span> <span class="nx">cancelURL</span> <span class="o">=</span> <span class="nx">event</span><span class="p">.</span><span class="nx">id</span> <span class="p">?</span> <span class="s2">`/events/</span><span class="p">${</span><span class="nx">event</span><span class="p">.</span><span class="nx">id</span><span class="p">}</span><span class="s2">`</span> <span class="p">:</span> <span class="dl">'</span><span class="s1">/events</span><span class="dl">'</span><span class="p">;</span>
<span class="kd">const</span> <span class="nx">title</span> <span class="o">=</span> <span class="nx">event</span><span class="p">.</span><span class="nx">id</span> <span class="p">?</span> <span class="s2">`</span><span class="p">${</span><span class="nx">event</span><span class="p">.</span><span class="nx">event_date</span><span class="p">}</span><span class="s2"> - </span><span class="p">${</span><span class="nx">event</span><span class="p">.</span><span class="nx">event_type</span><span class="p">}</span><span class="s2">`</span> <span class="p">:</span> <span class="dl">'</span><span class="s1">New Event</span><span class="dl">'</span><span class="p">;</span>
<span class="k">return</span> <span class="p">(</span>
<span class="p"><</span><span class="nt">div</span><span class="p">></span>
<span class="p"><</span><span class="nt">h2</span><span class="p">></span><span class="si">{</span><span class="nx">title</span><span class="si">}</span><span class="p"></</span><span class="nt">h2</span><span class="p">></span>
<span class="si">{</span><span class="k">this</span><span class="p">.</span><span class="nx">renderErrors</span><span class="p">()</span><span class="si">}</span>
<span class="p"><</span><span class="nt">form</span> <span class="na">className</span><span class="p">=</span><span class="s">"eventForm"</span> <span class="na">onSubmit</span><span class="p">=</span><span class="si">{</span><span class="k">this</span><span class="p">.</span><span class="nx">handleSubmit</span><span class="si">}</span><span class="p">></span>
...
<span class="p"><</span><span class="nt">div</span> <span class="na">className</span><span class="p">=</span><span class="s">"form-actions"</span><span class="p">></span>
<span class="p"><</span><span class="nt">button</span> <span class="na">type</span><span class="p">=</span><span class="s">"submit"</span><span class="p">></span>Save<span class="p"></</span><span class="nt">button</span><span class="p">></span>
<span class="p"><</span><span class="nc">Link</span> <span class="na">to</span><span class="p">=</span><span class="si">{</span><span class="nx">cancelURL</span><span class="si">}</span><span class="p">></span>Cancel<span class="p"></</span><span class="nc">Link</span><span class="p">></span>
<span class="p"></</span><span class="nt">div</span><span class="p">></span>
<span class="p"></</span><span class="nt">form</span><span class="p">></span>
<span class="p"></</span><span class="nt">div</span><span class="p">></span>
<span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>
<p>And the date validation in <code class="language-plaintext highlighter-rouge">helpers.js</code>:</p>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">isValidDate</span> <span class="o">=</span> <span class="nx">dateObj</span> <span class="o">=></span> <span class="o">!</span><span class="nb">Number</span><span class="p">.</span><span class="nb">isNaN</span><span class="p">(</span><span class="nb">Date</span><span class="p">.</span><span class="nx">parse</span><span class="p">(</span><span class="nx">dateObj</span><span class="p">));</span>
<span class="k">export</span> <span class="kd">const</span> <span class="nx">validateEvent</span> <span class="o">=</span> <span class="p">(</span><span class="nx">event</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="p">...</span>
<span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">isValidDate</span><span class="p">(</span><span class="nx">event</span><span class="p">.</span><span class="nx">event_date</span><span class="p">))</span> <span class="p">{</span>
<span class="nx">errors</span><span class="p">.</span><span class="nx">event_date</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">You must enter a valid date</span><span class="dl">'</span><span class="p">;</span>
<span class="p">}</span>
<span class="p">...</span>
<span class="k">return</span> <span class="nx">errors</span><span class="p">;</span>
<span class="p">};</span>
</code></pre></div></div>
<h2 id="adding-search">Adding Search</h2>
<p>It’d be nice to add search functionality to the events list. Luckily this is not complicated as we are holding all the events in state.</p>
<p>Let’s start off by adding a search input in our <code class="language-plaintext highlighter-rouge"><EventList></code> component:</p>
<div class="language-jsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">render</span><span class="p">()</span> <span class="p">{</span>
<span class="k">return</span> <span class="p">(</span>
<span class="p"><</span><span class="nt">section</span> <span class="na">className</span><span class="p">=</span><span class="s">"eventList"</span><span class="p">></span>
<span class="p"><</span><span class="nt">h2</span><span class="p">></span>
Events
<span class="p"><</span><span class="nc">Link</span> <span class="na">to</span><span class="p">=</span><span class="s">"/events/new"</span><span class="p">></span>New Event<span class="p"></</span><span class="nc">Link</span><span class="p">></span>
<span class="p"></</span><span class="nt">h2</span><span class="p">></span>
<span class="p"><</span><span class="nt">input</span>
<span class="na">className</span><span class="p">=</span><span class="s">"search"</span>
<span class="na">placeholder</span><span class="p">=</span><span class="s">"Search"</span>
<span class="na">type</span><span class="p">=</span><span class="s">"text"</span>
<span class="na">ref</span><span class="p">=</span><span class="si">{</span><span class="k">this</span><span class="p">.</span><span class="nx">searchInput</span><span class="si">}</span>
<span class="na">onKeyUp</span><span class="p">=</span><span class="si">{</span><span class="k">this</span><span class="p">.</span><span class="nx">updateSearchTerm</span><span class="si">}</span>
<span class="p">/></span>
<span class="p"><</span><span class="nt">ul</span><span class="p">></span><span class="si">{</span><span class="k">this</span><span class="p">.</span><span class="nx">renderEvents</span><span class="p">()</span><span class="si">}</span><span class="p"></</span><span class="nt">ul</span><span class="p">></span>
<span class="p"></</span><span class="nt">section</span><span class="p">></span>
<span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>
<p>Notice that we have added a ref to the input element, so that we can reference it within our component. Now let’s create that ref and declare a <code class="language-plaintext highlighter-rouge">searchTerm</code> property in state.</p>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">class</span> <span class="nx">EventList</span> <span class="kd">extends</span> <span class="nx">React</span><span class="p">.</span><span class="nx">Component</span> <span class="p">{</span>
<span class="kd">constructor</span><span class="p">(</span><span class="nx">props</span><span class="p">)</span> <span class="p">{</span>
<span class="k">super</span><span class="p">(</span><span class="nx">props</span><span class="p">);</span>
<span class="k">this</span><span class="p">.</span><span class="nx">state</span> <span class="o">=</span> <span class="p">{</span>
<span class="na">searchTerm</span><span class="p">:</span> <span class="dl">''</span><span class="p">,</span>
<span class="p">};</span>
<span class="k">this</span><span class="p">.</span><span class="nx">searchInput</span> <span class="o">=</span> <span class="nx">React</span><span class="p">.</span><span class="nx">createRef</span><span class="p">();</span>
<span class="k">this</span><span class="p">.</span><span class="nx">updateSearchTerm</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">updateSearchTerm</span><span class="p">.</span><span class="nx">bind</span><span class="p">(</span><span class="k">this</span><span class="p">);</span>
<span class="p">}</span>
<span class="nx">updateSearchTerm</span><span class="p">()</span> <span class="p">{</span>
<span class="k">this</span><span class="p">.</span><span class="nx">setState</span><span class="p">({</span> <span class="na">searchTerm</span><span class="p">:</span> <span class="k">this</span><span class="p">.</span><span class="nx">searchInput</span><span class="p">.</span><span class="nx">current</span><span class="p">.</span><span class="nx">value</span> <span class="p">});</span>
<span class="p">}</span>
<span class="p">...</span>
<span class="p">}</span>
</code></pre></div></div>
<p>We’re also creating a <code class="language-plaintext highlighter-rouge">updateSearchTerm</code> method which will be called every time a key press is registered on the search field.</p>
<p>The list of events is rendered in the <code class="language-plaintext highlighter-rouge">renderEvents</code> method. Let’s apply a filter to our event list, so that only events matching the search criteria are displayed:</p>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">renderEvents</span><span class="p">()</span> <span class="p">{</span>
<span class="kd">const</span> <span class="p">{</span> <span class="nx">activeId</span><span class="p">,</span> <span class="nx">events</span> <span class="p">}</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">props</span><span class="p">;</span>
<span class="kd">const</span> <span class="nx">filteredEvents</span> <span class="o">=</span> <span class="nx">events</span>
<span class="p">.</span><span class="nx">filter</span><span class="p">(</span><span class="nx">el</span> <span class="o">=></span> <span class="k">this</span><span class="p">.</span><span class="nx">matchSearchTerm</span><span class="p">(</span><span class="nx">el</span><span class="p">))</span>
<span class="p">.</span><span class="nx">sort</span><span class="p">((</span><span class="nx">a</span><span class="p">,</span> <span class="nx">b</span><span class="p">)</span> <span class="o">=></span> <span class="k">new</span> <span class="nb">Date</span><span class="p">(</span><span class="nx">b</span><span class="p">.</span><span class="nx">event_date</span><span class="p">)</span> <span class="o">-</span> <span class="k">new</span> <span class="nb">Date</span><span class="p">(</span><span class="nx">a</span><span class="p">.</span><span class="nx">event_date</span><span class="p">));</span>
<span class="k">return</span> <span class="nx">filteredEvents</span><span class="p">.</span><span class="nx">map</span><span class="p">(</span><span class="nx">event</span> <span class="o">=></span> <span class="p">(</span>
<span class="p">...</span>
<span class="p">));</span>
<span class="p">}</span>
</code></pre></div></div>
<p>And finally, we need the <code class="language-plaintext highlighter-rouge">matchSearchTerm</code> method:</p>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">matchSearchTerm</span><span class="p">(</span><span class="nx">obj</span><span class="p">)</span> <span class="p">{</span>
<span class="kd">const</span> <span class="p">{</span>
<span class="nx">id</span><span class="p">,</span> <span class="nx">published</span><span class="p">,</span> <span class="nx">created_at</span><span class="p">,</span> <span class="nx">updated_at</span><span class="p">,</span> <span class="p">...</span><span class="nx">rest</span>
<span class="p">}</span> <span class="o">=</span> <span class="nx">obj</span><span class="p">;</span>
<span class="kd">const</span> <span class="p">{</span> <span class="nx">searchTerm</span> <span class="p">}</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">state</span><span class="p">;</span>
<span class="k">return</span> <span class="nb">Object</span><span class="p">.</span><span class="nx">values</span><span class="p">(</span><span class="nx">rest</span><span class="p">).</span><span class="nx">some</span><span class="p">(</span>
<span class="nx">value</span> <span class="o">=></span> <span class="nx">value</span><span class="p">.</span><span class="nx">toLowerCase</span><span class="p">().</span><span class="nx">indexOf</span><span class="p">(</span><span class="nx">searchTerm</span><span class="p">.</span><span class="nx">toLowerCase</span><span class="p">())</span> <span class="o">></span> <span class="o">-</span><span class="mi">1</span><span class="p">,</span>
<span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>
<p>Here, we are excluding some database fields that were returned by the original Ajax call, but which we are not interested in filtering by.</p>
<p>Et voilà! We have search.</p>
<p><img src="https://res.cloudinary.com/hibbard/image/upload/v1549018226/event-manager/event-manager-07.png" alt="Event Manager - Search functionality" /></p>
<h2 id="adding-a-404-component">Adding a 404 Component</h2>
<p>The last thing we will do is add a component to render when an event is not found. This might be useful if a user has bookmarked an event which has since been deleted.</p>
<p>First, create the new component:</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">touch </span>app/javascript/components/EventNotFound.js
</code></pre></div></div>
<p>And add the content. Nothing exciting here:</p>
<div class="language-jsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="nx">React</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">react</span><span class="dl">'</span><span class="p">;</span>
<span class="kd">const</span> <span class="nx">EventNotFound</span> <span class="o">=</span> <span class="p">()</span> <span class="o">=></span> <span class="p"><</span><span class="nt">p</span><span class="p">></span>Event not found!<span class="p"></</span><span class="nt">p</span><span class="p">>;</span>
<span class="k">export</span> <span class="k">default</span> <span class="nx">EventNotFound</span><span class="p">;</span>
</code></pre></div></div>
<p>Now we need to update the <code class="language-plaintext highlighter-rouge"><Event></code> component:</p>
<div class="language-jsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="nx">EventNotFound</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">./EventNotFound</span><span class="dl">'</span><span class="p">;</span>
<span class="kd">const</span> <span class="nx">Event</span> <span class="o">=</span> <span class="p">({</span> <span class="nx">event</span><span class="p">,</span> <span class="nx">onDelete</span> <span class="p">})</span> <span class="o">=></span> <span class="p">{</span>
<span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">event</span><span class="p">)</span> <span class="k">return</span> <span class="p"><</span><span class="nc">EventNotFound</span> <span class="p">/>;</span>
<span class="k">return</span> <span class="p">(</span> <span class="p">...</span> <span class="p">);</span>
<span class="p">};</span>
</code></pre></div></div>
<p>And finally, the <code class="language-plaintext highlighter-rouge"><EventForm></code> component:</p>
<div class="language-jsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="nx">EventNotFound</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">./EventNotFound</span><span class="dl">'</span><span class="p">;</span>
<span class="kd">class</span> <span class="nx">EventForm</span> <span class="kd">extends</span> <span class="nx">React</span><span class="p">.</span><span class="nx">Component</span> <span class="p">{</span>
<span class="p">...</span>
<span class="nx">render</span><span class="p">()</span> <span class="p">{</span>
<span class="kd">const</span> <span class="p">{</span> <span class="nx">event</span> <span class="p">}</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">state</span><span class="p">;</span>
<span class="kd">const</span> <span class="p">{</span> <span class="nx">path</span> <span class="p">}</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">props</span><span class="p">;</span>
<span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">event</span><span class="p">.</span><span class="nx">id</span> <span class="o">&&</span> <span class="nx">path</span> <span class="o">===</span> <span class="dl">'</span><span class="s1">/events/:id/edit</span><span class="dl">'</span><span class="p">)</span> <span class="k">return</span> <span class="p"><</span><span class="nc">EventNotFound</span> <span class="p">/>;</span>
<span class="kd">const</span> <span class="nx">cancelURL</span> <span class="o">=</span> <span class="nx">event</span><span class="p">.</span><span class="nx">id</span> <span class="p">?</span> <span class="s2">`/events/</span><span class="p">${</span><span class="nx">event</span><span class="p">.</span><span class="nx">id</span><span class="p">}</span><span class="s2">`</span> <span class="p">:</span> <span class="dl">'</span><span class="s1">/events</span><span class="dl">'</span><span class="p">;</span>
<span class="kd">const</span> <span class="nx">title</span> <span class="o">=</span> <span class="nx">event</span><span class="p">.</span><span class="nx">id</span> <span class="p">?</span> <span class="s2">`</span><span class="p">${</span><span class="nx">event</span><span class="p">.</span><span class="nx">event_date</span><span class="p">}</span><span class="s2"> - </span><span class="p">${</span><span class="nx">event</span><span class="p">.</span><span class="nx">event_type</span><span class="p">}</span><span class="s2">`</span> <span class="p">:</span> <span class="dl">'</span><span class="s1">New Event</span><span class="dl">'</span><span class="p">;</span>
<span class="k">return</span> <span class="p">(</span> <span class="p">...</span> <span class="p">);</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="nx">EventForm</span><span class="p">.</span><span class="nx">propTypes</span> <span class="o">=</span> <span class="p">{</span>
<span class="na">event</span><span class="p">:</span> <span class="nx">PropTypes</span><span class="p">.</span><span class="nx">shape</span><span class="p">(),</span>
<span class="na">onSubmit</span><span class="p">:</span> <span class="nx">PropTypes</span><span class="p">.</span><span class="nx">func</span><span class="p">.</span><span class="nx">isRequired</span><span class="p">,</span>
<span class="na">path</span><span class="p">:</span> <span class="nx">PropTypes</span><span class="p">.</span><span class="nx">string</span><span class="p">.</span><span class="nx">isRequired</span><span class="p">,</span>
<span class="p">};</span>
</code></pre></div></div>
<p>Now if the user attempts to view or edit a non-existent event, they will be shown a 404 component.</p>
<h2 id="conclusion">Conclusion</h2>
<p>And that’s everything. Well done if you’ve made it up to here. You should now have a fully functioning React/Rails CRUD app.</p>
<p>I hope this tutorial has helped somebody, I’d be glad to hear your comments in the discussion below.</p>James HibbardA tutorial explaining how to create a Rails API then, using the Webpacker gem, build a React front-end to consume it. This is an older post which uses React classes.How to Update Phusion Passenger When Installed via RubyGems2018-07-19T00:00:00+00:002018-07-19T00:00:00+00:00https://hibbard.eu/how-to-update-passenger<figure>
<img src="https://res.cloudinary.com/hibbard/image/upload/f_auto,w_800/v1535634471/stock/passenger.jpg" alt="Pair of feet sticking out of a car window" />
<figcaption>Photo by <a href="https://unsplash.com/photos/Jx39BWpv6xs">Erik Odiin</a> on <a href="https://unsplash.com/search/photos/passenger">Unsplash</a></figcaption>
</figure>
<p>Phusion Passenger (a.k.a. mod_rails) is a module for the Apache HTTP Server which can (among other things) be used to deploy Rails apps.</p>
<p>As with any piece of software, from time to time <a href="https://blog.phusion.nl/tag/security%20advisory/">security vulnerabilities will be discovered</a> and Passenger will need to be updated.</p>
<p>Although the project’s homepage offers <a href="https://www.phusionpassenger.com/library/install/apache/upgrade/">some excellent documentation on how to do this</a>, the steps they describe didn’t work for me and resulted in my app crashing.</p>
<p>Inspecting the Apache error logs informed me that a segmentation fault had occurred and that I may have encountered a bug in the Ruby interpreter. This was accompanied by a 5,000 line stack trace.</p>
<p>Oh dear!</p>
<!--more-->
<h2 id="lets-start-at-the-beginning">Let’s Start at the Beginning</h2>
<p>If you installed Passenger via RubyGems, the update guide (linked to above) recommends that you simply repeat the normal installation process.</p>
<p>This is as follows:</p>
<h3 id="install-the-gem">Install the Gem</h3>
<p>Easy enough.</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>gem <span class="nb">install </span>passenger <span class="nt">--no-rdoc</span> <span class="nt">--no-ri</span>
</code></pre></div></div>
<p>Note that the <code class="language-plaintext highlighter-rouge">--no-rdoc --no-ri</code> argument makes installation faster by skipping generation of API documentation. You can also avoid specifying this every time you install a gem by adding this to a <code class="language-plaintext highlighter-rouge">.gemrc</code> file in your home directory and restarting your terminal.</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">cd</span> <span class="o">&&</span> <span class="nb">touch</span> .gemrc
<span class="nb">echo</span> <span class="s2">"gem: --no-rdoc --no-ri"</span> <span class="o">></span> .gemrc
<span class="nb">source</span> ~/.bashrc
</code></pre></div></div>
<h3 id="run-the-passenger-apache-module-installer">Run the Passenger Apache Module Installer</h3>
<p>Step 2 takes you into an install wizard, where you will be asked what you want to install (Ruby, Node etc).</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>passenger-install-apache2-module
</code></pre></div></div>
<p>Passenger will then compile the Apache module. Depending on how much RAM you have available, this might take some time.</p>
<p>Once Passenger is done, it’ll spit out a configuration snippet which you should paste into your Apache configuration file.</p>
<p>Finally, it’ll ask you to restart Apache with <code class="language-plaintext highlighter-rouge">sudo service apache2 reload</code>.</p>
<p>This is where my problems started …</p>
<h2 id="segmentation-fault">Segmentation Fault</h2>
<p>When I attempted to restart Apache, my app went into 404 mode and became unreachable.</p>
<p>Inspecting <code class="language-plaintext highlighter-rouge">/var/log/apache2/error.log</code> revealed a very long error message.</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>App 108939 output: /var/www/app/shared/bundle/ruby/2.5.0/gems/celluloid-0.17.3/lib/celluloid/mailbox.rb:41:
App 108939 output: <span class="o">[</span>BUG] Segmentation fault at 0x0000557b8e8ba731
App 108939 output: ruby 2.5.1p57 <span class="o">(</span>2018-03-29 revision 63029<span class="o">)</span> <span class="o">[</span>x86_64-linux]
App 108939 output:
App 108939 output: <span class="nt">--</span> Control frame information <span class="nt">-----------------------------------------------</span>
App 108939 output: c:0016 p:---- s:0076 e:000075 CFUNC :signal
App 108939 output: c:0015 p:0075 s:0072 e:000071 METHOD /var/www/app/shared/bundle/ruby/2.5.0/gems/celluloid-0.17.3/lib/celluloid/mailbox.rb:41
App 108939 output: c:0014 p:0055 s:0067 e:000066 METHOD /var/www/app/shared/bundle/ruby/2.5.0/gems/celluloid-0.17.3/lib/celluloid/proxy/actor.rb:35
...
</code></pre></div></div>
<p>This had me scratching my head until about three quarters of the way down I found the following lines mentioning a “graceful restart”:</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">[</span>Sat Jun 02 06:25:01.765420 2018] <span class="o">[</span>mpm_prefork:notice] <span class="o">[</span>pid 83397] AH00171: Graceful restart requested, doing restart
<span class="o">[</span> N 2018-06-02 06:25:01.7953 108669/T4 age/Cor/CoreMain.cpp:615 <span class="o">]</span>: Signal received. Gracefully shutting down... <span class="o">(</span>send signal 2 more <span class="nb">time</span><span class="o">(</span>s<span class="o">)</span> to force shutdown<span class="o">)</span>
<span class="o">[</span> N 2018-06-02 06:25:01.7954 108669/T1 age/Cor/CoreMain.cpp:1148 <span class="o">]</span>: Received <span class="nb">command </span>to shutdown gracefully. Waiting <span class="k">until </span>all clients have disconnected...
<span class="o">[</span> N 2018-06-02 06:25:01.7954 108669/T1 age/Cor/CoreMain.cpp:1062 <span class="o">]</span>: Checking whether to disconnect long-running connections <span class="k">for </span>process 127768, application /var/www/app/current <span class="o">(</span>production<span class="o">)</span>
</code></pre></div></div>
<p>I’d never heard of a graceful restart, but a quick Google search turned up <a href="https://benohead.com/apache2-graceful-restart-seg-fault-or-similar-nasty-error-detected-in-the-parent-process/">this page</a> which stated:</p>
<blockquote>
<p>The graceful signal causes the parent process to advise the children to exit after their current request (or to exit immediately if they’re not serving anything). The parent re-reads its configuration files and re-opens its log files. When a child dies off the parent replaces it with a child of the new generation of the configuration. This immediately begins serving new requests.</p>
</blockquote>
<p>So I tried sending a graceful signal to Apache, like so:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>apachectl graceful
</code></pre></div></div>
<p>and to my huge relief, the site came back up again.</p>
<p>After that I could complete the final step listed in the Passenger docs and verify the install:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>passenger-config validate-install
</code></pre></div></div>
<p>to which Passenger replied “Everything looks good. :-)”</p>
<p>Thanks passenger, I love you, too…</p>James HibbardPhoto by Erik Odiin on Unsplash Phusion Passenger (a.k.a. mod_rails) is a module for the Apache HTTP Server which can (among other things) be used to deploy Rails apps. As with any piece of software, from time to time security vulnerabilities will be discovered and Passenger will need to be updated. Although the project’s homepage offers some excellent documentation on how to do this, the steps they describe didn’t work for me and resulted in my app crashing. Inspecting the Apache error logs informed me that a segmentation fault had occurred and that I may have encountered a bug in the Ruby interpreter. This was accompanied by a 5,000 line stack trace. Oh dear!