- Ember JSZola2022-01-20T00:00:00+00:00https://www.fullstackstanley.com/tags/ember-js/atom.xmlDesktop GUIs for Web Developers2022-01-20T00:00:00+00:002022-01-20T00:00:00+00:00https://www.fullstackstanley.com/articles/desktop-guis-for-webdevelopers/<p><img src="/images/desktop-gui-banner.jpg" alt="" /></p>
<p>Over the past few years I’ve become more interested in making desktop applications. For some context, I’m a web developer with around 15 years of experience. I mostly work with Laravel and Vue.JS but I have dabbled in a whole heap of other languages and frameworks.</p>
<span id="continue-reading"></span>
<p>I love a good desktop app and where possible I’ll generally prefer having an app rather than visiting a website. I’ve also had to turn websites into desktop apps at my job so I’ve explored a fair few different technologies for this purpose.</p>
<p>I’ve written this blog to share the desktop technologies that I'm currently interested in. Bear in mind, some of these tools I’ve built full apps with and some I’ve only gone through briefly with tutorials. I’ll make this clear throughout the article.</p>
<p>I hope to have given you an idea of what to look for when choosing a desktop application framework. Hint: There is no golden ticket, each tool has pros and cons. All I can give you is my experience with each of them and when you should consider them for your projects.</p>
<p>The tools I'll be reviewing are:</p>
<ul>
<li>Compose Multiplatform</li>
<li>egui</li>
<li>Electron
<ul>
<li>Ember Electron</li>
<li>Quasar</li>
</ul>
</li>
<li>Flutter</li>
<li>React Native for Windows</li>
<li>Tauri</li>
</ul>
<h2 id="what-to-look-for-in-a-gui-tool">What to look for in a GUI tool</h2>
<p>There are almost as many GUI tools as there are frontend Javascript frameworks. So how do you pick one for the project you’re working on?</p>
<p>If you use a Javascript framework for the web, a good place to start is to see if there is a desktop counterpart for that library. For example, Quasar for Vue developers, <a href="https://microsoft.github.io/react-native-windows/">React Native</a> for React developers, Ember Electron for Ember developers and so on.</p>
<p>Two of the three mentioned above are Electron based tools and I think it’s worth pointing out, if you want something built fast with access to a large community, ecosystem and is regularly updated then Electron is absolutely worth investigating. It gets a lot of gripe because release builds have a large file size, it’s not as fast as native, and generally most apps don’t feel quite right, but these downsides can often be forgiven.</p>
<p>As with all of the tools that I mention below, you have to weigh up various concerns.</p>
<ul>
<li><strong>Who your application is for?</strong>—do the users care about it being a wrapper for a web application? Can the tool provide the functionality that your users expect?</li>
<li><strong>How complex is the project likely to be?</strong>—are there frequent updates that need to be kept up to date with web/mobile counter-parts?</li>
<li><strong>The size of the team working on the project</strong>—single developer or a large team? Trying to keep two code bases up to date (e.g. a website and a desktop app) for a single developer could literally half their productivity. Not so much of an issue for a small team.</li>
<li><strong>How fast does it need to be built?</strong> Exploring new technology takes time and some tools are easier to grok than others, have larger communities to help, and have plugins to solve various problems.</li>
<li><strong>Accessibility.</strong> Unless your making a personal project, you should try to add some level of accessibility to your application. The more the better, but not every tool makes this easy.</li>
</ul>
<p>With those key points in mind, there are a few additional things to think about</p>
<ul>
<li><strong>What platforms do you want to build for?</strong> Not all tools work on every platform. For example, React Native does not build for Linux but does work on iOS and Android. SwiftUI does not build for Linux or WIndows, but the code can be shared with all of the Apple Ecosystem.</li>
<li><strong>Distribution and updates.</strong> Do you want to distribute via Apple's App Store, the Microsoft Store? Linux has various options for automatic updates including Snaps and AppImages. For MacOS and Windows there’s also options for updates via your own server, or you could let user base to update manually.</li>
<li><strong>Support.</strong> Is the library actively maintained and how often is it updated?</li>
<li><strong>Should you choose boring technology?</strong> Small side projects can be a fun excuse to try a new stack, but if you're building a product for a company with customers reliant on stable software then you should probably pick something that’s been battle-tested.</li>
<li><strong>Level of native integration.</strong> Native isn’t necessarily boolean value. You can use web based technologies for the core application but still support native APIs for window management, menu/tray support, storage, notifications, widgets and more. Electron for example, has great options for all of those features. Some of the newer/smaller libraries tend to fall short in this regard.</li>
</ul>
<p>Finally, if you’re not familiar with a front end javascript library—maybe because you’re a backend developer—You might also want to look into libraries for programming languages that you’re familiar with. There are often wrappers for existing technologies like GTK, FLTK, Qt. For example, <a href="https://github.com/fltk-rs/fltk-rs">FLTK-rs</a> for Rust, or the <a href="https://www.rubydoc.info/gems/gtk3/3.4.3">GTK3 gem for Ruby</a>.</p>
<h2 id="so-what-s-out-there">So, what’s out there?</h2>
<p>Here comes the fun stuff. Obviously I can’t go through every single option available, but I will show you what has piqued my interest</p>
<h3 id="compose-multiplatform">Compose Multiplatform</h3>
<p><img src="https://res.craft.do/user/full/1151b1b2-72dd-5898-2b89-8130bb43a6e7/doc/3E58B53A-C376-4396-A034-62CC244259EE/F5900E3A-0221-415F-8C32-DAA56C31811E_2/kvjc8Y6ojiAySt3FNyjRvGctLJj4AcyxfwLoPV5LY18z/Image.png" alt="Image.png" /></p>
<p>Not to be confused with <a href="https://developer.android.com/jetpack/compose">Jetpack Compose</a>, the modern toolkit for building Android apps, JetBrains’ Compose Multiplatform is based on the same technology but allows you to build for Windows/MacOS, Linux <em>and</em> the web.</p>
<p>Compose uses Kotlin and my opinion, this language feels great. So far I’ve run through the <a href="https://www.raywenderlich.com/26791460-compose-for-desktop-get-your-weather">Ray Wenderlich tutorial by Roberto Orgiu</a> and I enjoyed the experience. However, there is a lack of resources available for learning it. This tutorial and the official docs and examples are the only things I've come across.</p>
<pre data-lang="kotlin" style="background-color:#2b303b;color:#c0c5ce;" class="language-kotlin "><code class="language-kotlin" data-lang="kotlin"><span style="color:#b48ead;">fun </span><span style="color:#8fa1b3;">main</span><span>() = Window(
</span><span> title = "</span><span style="color:#a3be8c;">Sunny Desk</span><span>",
</span><span> size = IntSize(</span><span style="color:#d08770;">800</span><span>, </span><span style="color:#d08770;">700</span><span>),
</span><span>) {
</span><span> </span><span style="color:#b48ead;">val </span><span>repository = Repository(</span><span style="color:#d08770;">API_KEY</span><span>)
</span><span>
</span><span> MaterialTheme {
</span><span> WeatherScreen(repository)
</span><span> }
</span><span>}
</span></code></pre>
<p>As mentioned on the website, it supports keyboard shortcuts, window manipulation, and notifications. It renders with Skia which means your apps will have native performance, however, you will need to build your own ‘Widgets’ or find an existing library if you want your app to actually <em>look</em> native to each platform.</p>
<p>Code sharing between Compose Multiplatform and the Jetpack Compose is possible, too, but I believe most of the UI elements have to be built separately. Still, this is a lot of platform support and I’m genuinely excited to see where this framework goes in the future.</p>
<p>Here’s some example code to get a feel of what it looks like</p>
<p><img src="https://res.craft.do/user/full/1151b1b2-72dd-5898-2b89-8130bb43a6e7/doc/3E58B53A-C376-4396-A034-62CC244259EE/147791a0-3922-46d5-9caa-0079805af89b" alt="image.png" /></p>
<pre data-lang="kotlin" style="background-color:#2b303b;color:#c0c5ce;" class="language-kotlin "><code class="language-kotlin" data-lang="kotlin"><span style="color:#b48ead;">import</span><span> androidx.compose.desktop.DesktopMaterialTheme
</span><span style="color:#b48ead;">import</span><span> androidx.compose.foundation.ContextMenuDataProvider
</span><span style="color:#b48ead;">import</span><span> androidx.compose.foundation.ContextMenuItem
</span><span style="color:#b48ead;">import</span><span> androidx.compose.foundation.layout.*
</span><span style="color:#b48ead;">import</span><span> androidx.compose.foundation.text.selection.SelectionContainer
</span><span style="color:#b48ead;">import</span><span> androidx.compose.material.Text
</span><span style="color:#b48ead;">import</span><span> androidx.compose.material.TextField
</span><span style="color:#b48ead;">import</span><span> androidx.compose.runtime.mutableStateOf
</span><span style="color:#b48ead;">import</span><span> androidx.compose.runtime.remember
</span><span style="color:#b48ead;">import</span><span> androidx.compose.ui.ExperimentalComposeUiApi
</span><span style="color:#b48ead;">import</span><span> androidx.compose.ui.Modifier
</span><span style="color:#b48ead;">import</span><span> androidx.compose.ui.unit.dp
</span><span style="color:#b48ead;">import</span><span> androidx.compose.ui.window.singleWindowApplication
</span><span>
</span><span>@OptIn(ExperimentalComposeUiApi::class, androidx.compose.foundation.ExperimentalFoundationApi::class)
</span><span style="color:#b48ead;">fun </span><span style="color:#8fa1b3;">main</span><span>() = singleWindowApplication(title = "</span><span style="color:#a3be8c;">Compose for Desktop</span><span>") {
</span><span> DesktopMaterialTheme { </span><span style="color:#65737e;">//it is mandatory for Context Menu
</span><span> </span><span style="color:#b48ead;">val </span><span>text = remember {mutableStateOf("</span><span style="color:#a3be8c;">Hello!</span><span>")}
</span><span> Column(modifier = Modifier.padding(all = </span><span style="color:#d08770;">5.</span><span>dp)) {
</span><span> ContextMenuDataProvider(
</span><span> items = {
</span><span> listOf(
</span><span> ContextMenuItem("</span><span style="color:#a3be8c;">User-defined Action</span><span>") {</span><span style="color:#65737e;">/*do something here*/</span><span>},
</span><span> )
</span><span> }
</span><span> ) {
</span><span> TextField(
</span><span> value = text.value,
</span><span> onValueChange = { text.value = it },
</span><span> label = { Text(text = "</span><span style="color:#a3be8c;">Input</span><span>") },
</span><span> modifier = Modifier.fillMaxWidth()
</span><span> )
</span><span>
</span><span> Spacer(Modifier.height(</span><span style="color:#d08770;">16.</span><span>dp))
</span><span>
</span><span> SelectionContainer {
</span><span> Text(text.value)
</span><span> }
</span><span> }
</span><span> }
</span><span> }
</span><span>}
</span></code></pre>
<h4 id="positives">Positives</h4>
<ul>
<li>Works on MacOS/WIndows/Linux and the web.</li>
<li>Support for sharing code on Android apps with Jetpack Compose</li>
<li>Uses Kotlin</li>
<li>Native performance</li>
<li>Built in Previews</li>
<li>Has tools for automated testing</li>
<li>Supported by Jetbrains</li>
<li>Actively developed</li>
</ul>
<h4 id="negatives">Negatives</h4>
<ul>
<li>Maturity - 1.0 only recently released</li>
<li>Small community</li>
<li>Only stand alone builds are currently supported (Although there is an Apple App Store PR), no sign of how to handle automatic updates.</li>
<li>Small ecosystem (plugins etc)</li>
<li>No native UI widgets or theming out of the box</li>
</ul>
<h3 id="egui">egui</h3>
<p>egui is a Rust library and builds natively with Glium (Or Glow) and WASM for the web. For native, It supports MacOS, Linux, Windows.</p>
<p>Out of the Rust GUI libraries available I think this one is my personal favourite. It’s self-described as easy to use and difficult to make mistakes. For someone like myself—who’s more of a visitor to the Rust language—it’s music to my ears.</p>
<p>It’s actively maintained, with a new release out literally an hour ago as of the creation of this sentence.</p>
<p>Here’s a snippet taken from one of the examples along with the newly added context menu support (Right clicking UI elements).</p>
<pre data-lang="rust" style="background-color:#2b303b;color:#c0c5ce;" class="language-rust "><code class="language-rust" data-lang="rust"><span>egui::CentralPanel::default().</span><span style="color:#96b5b4;">show</span><span>(ctx, |</span><span style="color:#bf616a;">ui</span><span>| {
</span><span> </span><span style="color:#65737e;">// The central panel the region left after adding TopPanel's and SidePanel's
</span><span>
</span><span> ui.</span><span style="color:#96b5b4;">heading</span><span>("</span><span style="color:#a3be8c;">eframe template</span><span>");
</span><span> ui.</span><span style="color:#96b5b4;">hyperlink</span><span>("</span><span style="color:#a3be8c;">https://github.com/emilk/eframe_template</span><span>");
</span><span> ui.</span><span style="color:#96b5b4;">add</span><span>(egui::github_link_file!(
</span><span> "</span><span style="color:#a3be8c;">https://github.com/emilk/eframe_template/blob/master/</span><span>",
</span><span> "</span><span style="color:#a3be8c;">Source code.</span><span>"
</span><span> ));
</span><span> </span><span style="color:#b48ead;">let</span><span> response = ui.</span><span style="color:#96b5b4;">add</span><span>(Label::new("</span><span style="color:#a3be8c;">Test Context Menu</span><span>").</span><span style="color:#96b5b4;">sense</span><span>(Sense::click()));
</span><span> response.</span><span style="color:#96b5b4;">context_menu</span><span>(|</span><span style="color:#bf616a;">ui</span><span>| {
</span><span> </span><span style="color:#b48ead;">if</span><span> ui.</span><span style="color:#96b5b4;">button</span><span>("</span><span style="color:#a3be8c;">Open</span><span>").</span><span style="color:#96b5b4;">clicked</span><span>() {
</span><span> ui.</span><span style="color:#96b5b4;">close_menu</span><span>();
</span><span> }
</span><span> ui.</span><span style="color:#96b5b4;">separator</span><span>();
</span><span> </span><span style="color:#b48ead;">if</span><span> ui.</span><span style="color:#96b5b4;">button</span><span>("</span><span style="color:#a3be8c;">Cancel</span><span>").</span><span style="color:#96b5b4;">clicked</span><span>() {
</span><span> ui.</span><span style="color:#96b5b4;">close_menu</span><span>();
</span><span> }
</span><span> });
</span><span>
</span><span> egui::warn_if_debug_build(ui);
</span><span>});
</span></code></pre>
<p><img src="https://res.craft.do/user/full/1151b1b2-72dd-5898-2b89-8130bb43a6e7/doc/3E58B53A-C376-4396-A034-62CC244259EE/E30AE296-2B93-45D1-88BC-FA516B261A28_2/vQecbGbBM9xJfdzCMXb57Rsl17qEjK5tBu4yJiXc7ioz/Image.png" alt="Image.png" /></p>
<h4 id="positives-1">Positives</h4>
<ul>
<li>Works on MacOS, WIndows and Linux and the web.</li>
<li>Built with Rust</li>
<li>Native performance</li>
<li>Actively developed</li>
<li>Support for multiple renderers</li>
</ul>
<h4 id="negatives-1">Negatives</h4>
<ul>
<li>Maturity - Not currently at a 1.0 release so the API is unstable and missing features</li>
<li>Small community</li>
<li>Only stand-alone builds are currently supported (Although there is an Apple App Store PR), no sign of how to handle automatic updates.</li>
<li>No ecosystem (plugins etc)</li>
<li>No native UI widgets or theming out of the box</li>
<li>No live preview</li>
</ul>
<h3 id="electron">Electron</h3>
<p><img src="https://res.craft.do/user/full/1151b1b2-72dd-5898-2b89-8130bb43a6e7/doc/3E58B53A-C376-4396-A034-62CC244259EE/A90F8135-9D9B-429A-9204-164A3A906A55_2/cqWS1KHuLHY3bdcHZT3WHSyc2ycKHxBOFkL52qI5VS0z/Image.png" alt="Image.png" /></p>
<p>I’ve built two and a half apps with Electron so it’s fair to say I have experienced first hand the positives and negatives of the platform. Electron is a tool that puts web technologies on the desktop via Chrome. With Electron you’ll most likely be writing every part of the app with Javascript or Typescript, although it’s certainly possible to switch this up, for example, 1Password recently switched their desktop app to Electron with a Rust backend.</p>
<p>I’ve used Electron with Ember Electron and with Quasar (Vue.JS). I'll talk more about both individually below, but as a general overview, Electron is fantastic and easy to recommend so long as you can put up with its shortcomings</p>
<h4 id="positives-2">Positives</h4>
<ul>
<li>Works on MacOS, Windows, and Linux</li>
<li>Since it’s wrapping a web app, you can likely share most, of the code base with an web app if you have one</li>
<li>Large community and ecosystem</li>
<li>Support for many forms of distribution, including automatic updates and various app stores</li>
<li>Has Chrome’s accessibility features built in</li>
<li>Supports multiple windows, and some native components such as dialog boxes, notifications, etc</li>
</ul>
<h4 id="negatives-2">Negatives</h4>
<ul>
<li>Large file size due to bundling Chrome</li>
<li>Generally slower than the alternatives</li>
<li>Web wrapper—projects will look and feel out of place with the operating system</li>
<li><a href="https://www.electronjs.org/docs/latest/tutorial/security">There are many security practices to follow to keep your app secure</a></li>
</ul>
<h4 id="ember-electron">Ember Electron</h4>
<p><img src="https://res.craft.do/user/full/1151b1b2-72dd-5898-2b89-8130bb43a6e7/doc/3E58B53A-C376-4396-A034-62CC244259EE/0504C5EA-3F32-407A-AA3A-28AF2079AA43_2/17JewWI1zxPNBxaX3zOEGaSMBV58gBfxZuyGPGDycYgz/Image.png" alt="Image.png" /></p>
<p>Ember is one of my favourite Javascript frameworks. I’ve built many web projects with it so it was natural for me to try a desktop app with it, too. My apps, <a href="https://snipline.io">Snipline 1 and 2</a>, are both built with Ember Electron so I have a reasonable amount of experience with it.</p>
<p>All of the positives and negatives from the Electron section still apply here, so I’ll comment specifically of the Ember Electron add-on.</p>
<p>With Ember Electron 2, it was tricky to update the Electron dependency, but with the release of Ember Electron 3 the Electron Forge dependency was updated . This means that Electron can be kept up to date separately to Ember Electron. Since Electron gets updated quite regularly it's a much welcome update.</p>
<p>Activity is much slower now with Ember Electron, with the latest release of 3.1.0 back in May, and the community is very small compared to the other available choices. As much as I enjoy the stack, I could not recommend it unless you want to turn an existing Ember app into a desktop app, or are already very productive with Ember.</p>
<h4 id="quasar">Quasar</h4>
<p><img src="https://res.craft.do/user/full/1151b1b2-72dd-5898-2b89-8130bb43a6e7/doc/3E58B53A-C376-4396-A034-62CC244259EE/D40A2D8E-CD49-4EC5-9BCC-EE65F90F8BBB_2/Yq9YbzOcmTxcJOiqaLM4c9CHcinyBlytlncYJpAgVWoz/Image.png" alt="Image.png" /></p>
<p>Calling Quasar an Electron wrapper is selling it short. It provides many of the benefits of Ember JS, such as file directory conventions and a CLI, but it also adds support for mobile apps, SPAs and it’s own UI framework. Take a look at all of the reasons that make Quasar great on their <a href="https://quasar.dev/introduction-to-quasar">Why Quasar?</a> page.</p>
<p>I’ve built one desktop app with Quasar for an internal company project, and overall it was a pleasant experience. I much prefer Tailwind CSS to Quasar UI, and there’s nothing stopping you using both except for the additional dependency.</p>
<p>As with Ember Electron, you get all of the benefits of Electron with Quasar, and building the app is as simple as running a command</p>
<p><code>quasar build -m electron</code></p>
<p>One difference from Ember Electron is the build module. Ember Electron uses ‘Electron Forge’ whereas Quasar gives you two choices, Packager or Builder. Personally, I’ve used Builder and had no issues besides the teething problems of getting auto updating working on Windows.</p>
<p>Regarding activity, Quasar is very active, with an update to the main repo just for days ago as of writing and plenty recently before that. There are many contributors, and the documentation is great. I think that if you’re familiar with Vue.JS and Vuex then you’re in safe hands using Quasar.</p>
<h3 id="flutter">Flutter</h3>
<p><img src="https://res.craft.do/user/full/1151b1b2-72dd-5898-2b89-8130bb43a6e7/doc/3E58B53A-C376-4396-A034-62CC244259EE/A6808A14-1727-4CC2-B4FD-9C904211635A_2/udX0W4TcCe5ZvZAxzAoMw0T67TXTdYCOocEpPYyOykIz/Image.png" alt="Image.png" /></p>
<p>One of the most impressive things about Flutter is the breadth of devices it supports. From mobile, desktop, to embedded devices. Similar to Compose, it uses Skia to render the UI, so while you’re getting native <em>performance</em> you’re most likely not going to get a native <em>look</em>, at least not out of the box.</p>
<p>Unlike Compose, I was pleasantly surprised when I followed an Android tutorial to build a Windows app and it just <em>worked</em>. Of course, it looked like an Android app, with the default Material theme, but there's nothing to stop you tweaking the theme per device. <a href="https://blog.whidev.com/native-looking-desktop-app-with-flutter/">Take a look at this blog post by Minas Giannekas on how he built Shortcut Keeper and how he themed it for each platform</a>. Truly impressive.</p>
<p><img src="https://res.craft.do/user/full/1151b1b2-72dd-5898-2b89-8130bb43a6e7/doc/3E58B53A-C376-4396-A034-62CC244259EE/69DEB148-2A26-4627-AA20-52A20334C696_2/xAqExkZjbxmcZbzexUce86eYxCiS9MSTAObsziEmAmYz/Image" alt="Image" /></p>
<p>There’s also a large community and ecosystem surrounding Flutter, so you’re unlikely to run out of learning resources.</p>
<p>But Flutter isn’t without it’s shortcomings. There’s a long list of issues in their Github repo, which also speaks for the popularity of the library. Much of the ecosystem is focused on mobile, which means if you wish to make an app work across mobile, desktop, and web, you may have to provide your own functionality for the latter two environments.</p>
<p>There are also complaints that the development of Flutter outpaces the plugins surrounding it. You may need to stay on an older version of Flutter because of plugin compatibility issues.</p>
<h4 id="positives-3">Positives</h4>
<ul>
<li>Native performance</li>
<li>Works on MacOS, Windows, Linux, iOS, Android, and Embedded devices</li>
<li>Large community and lots of plugins</li>
<li>Actively developed and backed by Google</li>
<li>Large pool of resources to learn from</li>
</ul>
<h4 id="negatives-3">Negatives</h4>
<ul>
<li>Most of the community and plugins are mobile focused</li>
<li>Fast pace of development may mean compatibility issues with plugins</li>
</ul>
<h3 id="reactive-native-for-windows">Reactive Native for Windows</h3>
<p><img src="https://res.craft.do/user/full/1151b1b2-72dd-5898-2b89-8130bb43a6e7/doc/3E58B53A-C376-4396-A034-62CC244259EE/E0093C9B-A25E-485E-A967-3D7E84D7A2BC_2/MxYznMRTjIjX24ZefqIW67yy3c50zJFn7YFulVsQpzQz/Image.png" alt="Image.png" /></p>
<p>Since I’ve included a Vue.JS and an Ember JS library, I thought it would only be fair to include a library for React developers, too. React Native is a popular solution for building native apps for iOS and Android and uses Objective-C and Java under the hood for each platform respectively.</p>
<p>For Windows, it renders with Universal Windows Platform (Or UWP for short) which means you really are getting native controls rendered. I couldn’t find any information of how the React Native for MacOS renders, although I’d imagine it’s doing something similar to iOS.</p>
<p>Here’s a quick snippet that I tried starting from the base RNW project.</p>
<p><img src="https://res.craft.do/user/full/1151b1b2-72dd-5898-2b89-8130bb43a6e7/doc/3E58B53A-C376-4396-A034-62CC244259EE/13e15cc3-3f0a-4aae-ae7e-bee539b8ec85" alt="Screenshot 2022-01-01 165853.png" /></p>
<pre data-lang="javascript" style="background-color:#2b303b;color:#c0c5ce;" class="language-javascript "><code class="language-javascript" data-lang="javascript"><span style="color:#b48ead;">import </span><span style="color:#bf616a;">React</span><span>, { </span><span style="color:#bf616a;">useState </span><span>} </span><span style="color:#b48ead;">from </span><span>'</span><span style="color:#a3be8c;">react</span><span>';
</span><span style="color:#b48ead;">import type </span><span>{</span><span style="color:#bf616a;">Node</span><span>} </span><span style="color:#b48ead;">from </span><span>'</span><span style="color:#a3be8c;">react</span><span>';
</span><span style="color:#b48ead;">import </span><span>{
</span><span> </span><span style="color:#bf616a;">SafeAreaView</span><span>,
</span><span> </span><span style="color:#bf616a;">ScrollView</span><span>,
</span><span> </span><span style="color:#bf616a;">StatusBar</span><span>,
</span><span> </span><span style="color:#bf616a;">StyleSheet</span><span>,
</span><span> </span><span style="color:#bf616a;">Text</span><span>,
</span><span> </span><span style="color:#bf616a;">useColorScheme</span><span>,
</span><span> </span><span style="color:#bf616a;">View</span><span>,
</span><span> </span><span style="color:#bf616a;">Alert</span><span>,
</span><span> </span><span style="color:#bf616a;">Modal</span><span>,
</span><span> </span><span style="color:#bf616a;">Pressable
</span><span>} </span><span style="color:#b48ead;">from </span><span>'</span><span style="color:#a3be8c;">react-native</span><span>';
</span><span style="color:#b48ead;">import </span><span>{
</span><span> </span><span style="color:#bf616a;">Colors</span><span>,
</span><span> </span><span style="color:#bf616a;">DebugInstructions</span><span>,
</span><span> </span><span style="color:#bf616a;">Header</span><span>,
</span><span> </span><span style="color:#bf616a;">LearnMoreLinks</span><span>,
</span><span> </span><span style="color:#bf616a;">ReloadInstructions</span><span>,
</span><span>} </span><span style="color:#b48ead;">from </span><span>'</span><span style="color:#a3be8c;">react-native/Libraries/NewAppScreen</span><span>';
</span><span>
</span><span style="color:#b48ead;">const </span><span style="color:#8fa1b3;">Section </span><span>= ({</span><span style="color:#bf616a;">children</span><span>, </span><span style="color:#bf616a;">title</span><span>}): Node </span><span style="color:#b48ead;">=> </span><span>{
</span><span> </span><span style="color:#b48ead;">const </span><span style="color:#bf616a;">isDarkMode </span><span>= </span><span style="color:#8fa1b3;">useColorScheme</span><span>() === '</span><span style="color:#a3be8c;">dark</span><span>';
</span><span> </span><span style="color:#b48ead;">return </span><span>(
</span><span> {</span><span style="color:#bf616a;">title</span><span>} {</span><span style="color:#bf616a;">children</span><span>}
</span><span> );
</span><span>};
</span><span style="color:#b48ead;">const </span><span style="color:#8fa1b3;">App</span><span>: () </span><span style="color:#b48ead;">=> </span><span>Node = () </span><span style="color:#b48ead;">=> </span><span>{
</span><span> </span><span style="color:#b48ead;">const </span><span style="color:#bf616a;">isDarkMode </span><span>= </span><span style="color:#8fa1b3;">useColorScheme</span><span>() === '</span><span style="color:#a3be8c;">dark</span><span>';
</span><span> </span><span style="color:#b48ead;">const </span><span>[</span><span style="color:#bf616a;">timesPressed</span><span>, </span><span style="color:#bf616a;">setTimesPressed</span><span>] = </span><span style="color:#8fa1b3;">useState</span><span>(</span><span style="color:#d08770;">0</span><span>);
</span><span> </span><span style="color:#b48ead;">const </span><span style="color:#bf616a;">backgroundStyle </span><span>= {
</span><span> [</span><span style="color:#bf616a;">backgroundcolor</span><span>: </span><span style="color:#bf616a;">isDarkMode </span><span>? </span><span style="color:#bf616a;">Colors</span><span>.</span><span style="color:#bf616a;">darker </span><span>: </span><span style="color:#bf616a;">Colors</span><span>.</span><span style="color:#bf616a;">lighter</span><span>,
</span><span> };
</span><span> </span><span style="color:#bf616a;">const buttonStyle </span><span>= {
</span><span> [</span><span style="color:#bf616a;">padding</span><span>: '</span><span style="color:#a3be8c;">20px</span><span>',](</span><span style="color:#bf616a;">padding</span><span>: '</span><span style="color:#a3be8c;">20px</span><span>',)
</span><span> }
</span><span> </span><span style="color:#8fa1b3;">return </span><span>(
</span><span> <</span><span style="color:#bf616a;">SafeAreaView style</span><span>={</span><span style="color:#bf616a;">backgroundStyle</span><span>}>
</span><span> <</span><span style="color:#bf616a;">StatusBar barStyle</span><span>={isDarkMode ? '</span><span style="color:#a3be8c;">light-content</span><span>' : '</span><span style="color:#a3be8c;">dark-content</span><span>'} />
</span><span> <</span><span style="color:#bf616a;">ScrollView
</span><span> </span><span style="color:#bf616a;">contentInsetAdjustmentBehavior</span><span>="</span><span style="color:#a3be8c;">automatic</span><span>"
</span><span> </span><span style="color:#bf616a;">style</span><span>={</span><span style="color:#bf616a;">backgroundStyle</span><span>}>
</span><span> <</span><span style="color:#bf616a;">Section title</span><span>="</span><span style="color:#a3be8c;">React Native for Windows</span><span>"></Section>
</span><span> <</span><span style="color:#bf616a;">Pressable
</span><span> </span><span style="color:#bf616a;">onPress</span><span>={() => {
</span><span> </span><span style="color:#8fa1b3;">setTimesPressed</span><span>((</span><span style="color:#bf616a;">current</span><span>) => current + 1);
</span><span> }}
</span><span> </span><span style="color:#bf616a;">style</span><span>="</span><span style="color:#a3be8c;">{({pressed}) => </span><span style="background-color:#bf616a;color:#2b303b;">[</span><span>
</span><span> {
</span><span> backgroundColor: </span><span style="color:#bf616a;">pressed </span><span>? '</span><span style="color:#a3be8c;">rgb(210, 230, 255)</span><span>'
</span><span> : '</span><span style="color:#a3be8c;">black</span><span>',
</span><span> padding: </span><span style="color:#d08770;">10</span><span>,
</span><span> textAlign: '</span><span style="color:#a3be8c;">center</span><span>'
</span><span> },
</span><span> </span><span style="color:#bf616a;">styles</span><span>.</span><span style="color:#bf616a;">wrapperCustom
</span><span> ]}>
</span><span> {({ </span><span style="color:#bf616a;">pressed </span><span>}) </span><span style="color:#b48ead;">=> </span><span>(
</span><span> <</span><span style="color:#ebcb8b;">Text </span><span style="color:#bf616a;">style</span><span>={() => [ { ...</span><span style="color:#bf616a;">styles</span><span>.text, textAlign: '</span><span style="color:#a3be8c;">center</span><span>' }]}>
</span><span> {pressed ? 'Pressed!' : `</span><span style="color:#a3be8c;">Count: ${</span><span style="color:#bf616a;">timesPressed</span><span style="color:#a3be8c;">}</span><span>`}
</span><span> </</span><span style="color:#ebcb8b;">Text</span><span>>
</span><span> )}
</span><span> </</span><span style="color:#bf616a;">Pressable</span><span>>
</span><span> </</span><span style="color:#bf616a;">ScrollView</span><span>>
</span><span> </</span><span style="color:#bf616a;">SafeAreaView</span><span>>
</span><span> );
</span><span>};
</span><span>
</span><span style="color:#bf616a;">const styles </span><span>= </span><span style="color:#ebcb8b;">StyleSheet</span><span>.</span><span style="color:#8fa1b3;">create</span><span>({
</span><span> sectioncontainer: {
</span><span> margintop: </span><span style="color:#d08770;">32</span><span>,
</span><span> paddinghorizontal: </span><span style="color:#d08770;">24
</span><span> },
</span><span> sectiontitle:
</span><span> </span><span style="color:#bf616a;">fontsize</span><span>: </span><span style="color:#d08770;">24</span><span>,
</span><span> fontweight: '</span><span style="color:#a3be8c;">600</span><span>',
</span><span> },
</span><span> </span><span style="color:#bf616a;">sectiondescription</span><span>: {
</span><span> margintop: </span><span style="color:#d08770;">8</span><span>,
</span><span> fontsize: </span><span style="color:#d08770;">18</span><span>,
</span><span> fontweight: '</span><span style="color:#a3be8c;">400</span><span>',
</span><span> },
</span><span> </span><span style="color:#bf616a;">highlight</span><span>: {
</span><span> fontweight: '</span><span style="color:#a3be8c;">700</span><span>',
</span><span> },
</span><span>});
</span><span>
</span><span style="color:#bf616a;">export default App</span><span>;
</span></code></pre>
<p>In terms of community, you have the foundation of the mobile RN community to work with, but as with other ecosystems in this article, you probably aren’t going to find much plugin support for desktop at the moment.</p>
<h4 id="positives-4">Positives</h4>
<ul>
<li>Renders natively</li>
<li>Share code with React Native mobile apps</li>
<li>Build with Javascript or Typescript</li>
<li>React developers will feel right at home</li>
</ul>
<h4 id="negatives-4">Negatives</h4>
<ul>
<li>RNW and MacOS are relatively new and not yet stable</li>
<li>Smaller community and ecosystem for desktop</li>
<li>No Linux support</li>
</ul>
<h3 id="swiftui">SwiftUI</h3>
<p><img src="https://res.craft.do/user/full/1151b1b2-72dd-5898-2b89-8130bb43a6e7/doc/3E58B53A-C376-4396-A034-62CC244259EE/C39BAB3C-89CC-454F-8337-468BB4DC2867_2/xwysTbzKuSRA1qxyYTApzuS7OTBa0xwzWVxwtzHi67Mz/Image.png" alt="Image.png" /></p>
<p>Having released 2 apps and another on the way, SwiftUI is another tool I have plenty of experience with.</p>
<p>SwiftUI has been designed by Apple to work well on each of their platforms. There are many 'Widgets' that can be shared across each platform so you can write code once and have it run on most devices. For example, context menu's on an iOS device are triggered from a long press, where as on a Mac it's triggered from a right click.</p>
<pre data-lang="swift" style="background-color:#2b303b;color:#c0c5ce;" class="language-swift "><code class="language-swift" data-lang="swift"><span style="color:#65737e;">// Taken from the useful app, SwiftUI Companion
</span><span style="color:#b48ead;">struct</span><span> ExampleView: View {
</span><span> </span><span style="color:#b48ead;">var</span><span> body: some View {
</span><span> Text(</span><span style="color:#a3be8c;">"Press, hold and release"</span><span>)
</span><span> .foregroundColor(.white)
</span><span> .padding(</span><span style="color:#d08770;">15</span><span>)
</span><span> .background(RoundedRectangle(cornerRadius: </span><span style="color:#d08770;">8</span><span>).fill(Color.blue))
</span><span> .contextMenu {
</span><span> Button(</span><span style="color:#a3be8c;">"Open"</span><span>) { print(</span><span style="color:#a3be8c;">"open..."</span><span>) }
</span><span> Button(</span><span style="color:#a3be8c;">"Delete"</span><span>) { print(</span><span style="color:#a3be8c;">"delete..."</span><span>) }
</span><span> Button(</span><span style="color:#a3be8c;">"More info..."</span><span>) { print(</span><span style="color:#a3be8c;">"more..."</span><span>) }
</span><span> }
</span><span> }
</span><span>}
</span></code></pre>
<p>A personal favourite feature of mine, which I’ve yet to see in other GUI frameworks, is data binding between multiple windows. Using the <code>@AppStorage</code> property wrapper, you can update a value in one window and have it's value easily sync in another. This is <em>really</em> useful for preferences which are generally in their own window in MacOS apps.</p>
<p>Here’s a truncated example of the power and simplicity of SwiftUI for Mac apps.</p>
<pre data-lang="swift" style="background-color:#2b303b;color:#c0c5ce;" class="language-swift "><code class="language-swift" data-lang="swift"><span style="color:#b48ead;">import </span><span>SwiftUI
</span><span>
</span><span style="color:#b48ead;">@main
</span><span style="color:#b48ead;">struct</span><span> RsyncinatorApp: App {
</span><span> </span><span style="color:#b48ead;">@AppStorage</span><span>('showVisualHints') </span><span style="color:#b48ead;">private var</span><span> showVisualHints = </span><span style="color:#b48ead;">true
</span><span>
</span><span> </span><span style="color:#b48ead;">var</span><span> body: some Scene {
</span><span> WindowGroup {
</span><span> ContentView()
</span><span> }
</span><span>
</span><span> #</span><span style="color:#b48ead;">if</span><span> os(macOS)
</span><span> Settings {
</span><span> SettingsView()
</span><span> }
</span><span> #endif
</span><span> }
</span><span>}
</span><span>
</span><span style="color:#b48ead;">struct</span><span> SettingsView: View {
</span><span> </span><span style="color:#b48ead;">private enum</span><span> Tabs: Hashable {
</span><span> </span><span style="color:#b48ead;">case</span><span> general, advanced
</span><span> }
</span><span> </span><span style="color:#b48ead;">var</span><span> body: some View {
</span><span> TabView {
</span><span> GeneralSettingsView()
</span><span> .tabItem {
</span><span> Label(</span><span style="color:#a3be8c;">"General"</span><span>, systemImage: </span><span style="color:#a3be8c;">"gear"</span><span>)
</span><span> }
</span><span> .tag(Tabs.general)
</span><span> }
</span><span> .padding(</span><span style="color:#d08770;">20</span><span>)
</span><span> .frame(width: </span><span style="color:#d08770;">375</span><span>, height: </span><span style="color:#d08770;">150</span><span>)
</span><span> }
</span><span>}
</span><span>
</span><span style="color:#b48ead;">struct</span><span> GeneralSettingsView: View {
</span><span> </span><span style="color:#b48ead;">@AppStorage</span><span>(</span><span style="color:#a3be8c;">"showVisualHints"</span><span>) </span><span style="color:#b48ead;">private var</span><span> showVisualHints = </span><span style="color:#b48ead;">true
</span><span>
</span><span> </span><span style="color:#b48ead;">var</span><span> body: some View {
</span><span> Form {
</span><span> Toggle(</span><span style="color:#a3be8c;">"Show visual hints"</span><span>, isOn: $showVisualHints)
</span><span> }
</span><span> .padding(</span><span style="color:#d08770;">20</span><span>)
</span><span> .frame(width: </span><span style="color:#d08770;">350</span><span>, height: </span><span style="color:#d08770;">100</span><span>)
</span><span> }
</span><span>}
</span></code></pre>
<p>Here’s the Preferences window that’s generated. If you’re familiar with Mac apps, you should recognise the general layout with the tabbed sections at the top. All this is laid out for you.</p>
<p><img src="https://res.craft.do/user/full/1151b1b2-72dd-5898-2b89-8130bb43a6e7/doc/3E58B53A-C376-4396-A034-62CC244259EE/4269F254-4B2A-4838-93F9-D9F6A5F5EBFB_2/JVmsG5gbeSkqUr5rhV7sA9s780ZkBDy1riLroKNsZLwz/Image.png" alt="Image.png" /></p>
<p>One major show stopper for many people is that it doesn’t build for Windows and Linux. I also feel it’s only just becoming a <em>real</em> solution as of it's 3rd major release which adds much needed functionality. Functionality such as search and focus states weren't properly supported before so you'd have to write it yourself. There are also bugs that crop up and it's down to Apple's discretion as to when these get fixed.</p>
<p>The community and packages surrounding SwiftUI tend to focus on mobile, however, there are still a reasonable amount of resources for MacOS. If you're interested, <a href="https://developer.apple.com/tutorials/swiftui/creating-a-macos-app">take a look at this official tutorial for MacOS</a> for getting started.</p>
<h4 id="positives-5">Positives</h4>
<ul>
<li>Easy to make native Mac apps that <em>look</em> like Mac apps</li>
<li>Lots of resources for learning that are applicable for iOS and MacOS</li>
<li>Share code between iOS, tvOS, watchOS</li>
</ul>
<h4 id="negatives-5">Negatives</h4>
<ul>
<li>No build for Windows or Linux</li>
<li>Bugs are fixed at the whim of Apple</li>
<li>Only one major release per year with new functionality</li>
<li>Closed source</li>
<li>Only fairly recent MacOS versions support it and each of the previous versions of MacOS support fewer features</li>
</ul>
<h3 id="tauri">Tauri</h3>
<p><img src="https://res.craft.do/user/full/1151b1b2-72dd-5898-2b89-8130bb43a6e7/doc/3E58B53A-C376-4396-A034-62CC244259EE/68B0A145-E79B-4948-B8BB-E9D03CEF204E_2/WkBtfxfH3bSyuWoTuD9O0tbdyG2RRlQcpreibmcz6Sgz/Image.png" alt="Image.png" /></p>
<p>Tauri is another fairly new library. It’s a web wrapper and you can use whichever web framework you prefer. There’s an officially supported plugin for Vue.JS, but it is simple enough to add your own. I've had it working with both Ember JS and Svelte.</p>
<p>It’s first major difference from Electron is that it uses your Operating System’s web browser rather than bundling Chrome. This results in fairly tiny file sizes, but at the cost of having to debug issues on different platforms.</p>
<p>The second major difference is that Tauri uses Rust. With Electron you pass messages from main and renderer with Node and Javascript, whereas with Tauri you pass events from the frontend and backend with Javascript and Rust, respectively.</p>
<p>Here’s a snippet from the Tauri documentation of communicating between the two.</p>
<pre data-lang="javascript" style="background-color:#2b303b;color:#c0c5ce;" class="language-javascript "><code class="language-javascript" data-lang="javascript"><span style="color:#b48ead;">import </span><span>{ </span><span style="color:#bf616a;">getCurrent</span><span>, </span><span style="color:#bf616a;">WebviewWindow </span><span>} </span><span style="color:#b48ead;">from </span><span>'</span><span style="color:#a3be8c;">@tauri-apps/api/window</span><span>'
</span><span>
</span><span style="color:#65737e;">// emit an event that are only visible to the current window
</span><span style="color:#b48ead;">const </span><span style="color:#bf616a;">current </span><span>= </span><span style="color:#8fa1b3;">getCurrent</span><span>()
</span><span style="color:#bf616a;">current</span><span>.</span><span style="color:#8fa1b3;">emit</span><span>('</span><span style="color:#a3be8c;">event</span><span>', { message: '</span><span style="color:#a3be8c;">Tauri is awesome!</span><span>' })
</span><span>
</span><span style="color:#65737e;">// create a new webview window and emit an event only to that window
</span><span style="color:#b48ead;">const </span><span style="color:#bf616a;">webview </span><span>= new WebviewWindow('</span><span style="color:#a3be8c;">window</span><span>')
</span><span style="color:#bf616a;">webview</span><span>.</span><span style="color:#8fa1b3;">emit</span><span>('</span><span style="color:#a3be8c;">event</span><span>')
</span></code></pre>
<pre data-lang="rust" style="background-color:#2b303b;color:#c0c5ce;" class="language-rust "><code class="language-rust" data-lang="rust"><span style="color:#65737e;">// the payload type must implement `Serialize`.
</span><span style="color:#65737e;">// for global events, it also must implement `Clone`.
</span><span>#[</span><span style="color:#bf616a;">derive</span><span>(Clone, serde::Serialize)]
</span><span style="color:#b48ead;">struct </span><span>Payload {
</span><span> </span><span style="color:#bf616a;">message</span><span>: String,
</span><span>}
</span><span>
</span><span style="color:#b48ead;">fn </span><span style="color:#8fa1b3;">main</span><span>() {
</span><span> tauri::Builder::default()
</span><span> .</span><span style="color:#96b5b4;">setup</span><span>(|</span><span style="color:#bf616a;">app</span><span>| {
</span><span> </span><span style="color:#65737e;">// listen to the `event-name` (emitted on any window)
</span><span> </span><span style="color:#b48ead;">let</span><span> id = app.</span><span style="color:#96b5b4;">listen_global</span><span>("</span><span style="color:#a3be8c;">event-name</span><span>", |</span><span style="color:#bf616a;">event</span><span>| {
</span><span> println!("</span><span style="color:#a3be8c;">got event-name with payload </span><span style="color:#d08770;">{:?}</span><span>", event.</span><span style="color:#96b5b4;">payload</span><span>());
</span><span> });
</span><span> </span><span style="color:#65737e;">// unlisten to the event using the `id` returned on the `listen_global` function
</span><span> </span><span style="color:#65737e;">// an `once_global` API is also exposed on the `App` struct
</span><span> app.</span><span style="color:#96b5b4;">unlisten</span><span>(id);
</span><span>
</span><span> </span><span style="color:#65737e;">// emit the `event-name` event to all webview windows on the frontend
</span><span> app.</span><span style="color:#96b5b4;">emit_all</span><span>("</span><span style="color:#a3be8c;">event-name</span><span>", Payload { message: "</span><span style="color:#a3be8c;">Tauri is awesome!</span><span>".</span><span style="color:#96b5b4;">into</span><span>() }).</span><span style="color:#96b5b4;">unwrap</span><span>();
</span><span> Ok(())
</span><span> })
</span><span> .</span><span style="color:#96b5b4;">run</span><span>(tauri::generate_context!())
</span><span> .</span><span style="color:#96b5b4;">expect</span><span>("</span><span style="color:#a3be8c;">failed to run app</span><span>");
</span><span>}
</span></code></pre>
<p>I’ve built and released one app with Tauri and it was fairly painless for a simple app. I used Svelte for the web framework and each installer came out at less than 5MB.</p>
<p><img src="https://res.craft.do/user/full/1151b1b2-72dd-5898-2b89-8130bb43a6e7/doc/3E58B53A-C376-4396-A034-62CC244259EE/C06B5D4E-361E-47B4-BFBD-5EE4144BB008_2/4UPVE21KHfVURoUbWBvyPVcns6u8C4zZ3fjjn7gmSkoz/Image" alt="Image" /></p>
<p>For larger apps, I would most likely struggle to implement certain functionality. The getting started guides are easy enough to follow, but once I started trying to add more functionality I found the overall documentation lacking. There’s also fewer features than Electron which is to be expected since the platform is not as mature and the community not as large.</p>
<p>It supports adding CLI’s to your app which I think is a very cool feature that’s not often built into GUI libraries. You can also embed external binaries which can be very useful if you need to use a command-line tool for functionality in your app. It also supports auto updating for each platform (With Linux supporting AppImage).</p>
<h4 id="positives-6">Positives</h4>
<ul>
<li>Supports auto updating on MacOS, Windows and Linux</li>
<li>Build your own companion CLI</li>
<li>Integrate external binaries</li>
<li>Small distribution file sizes</li>
<li>Use any frontend JS framework you prefer</li>
</ul>
<h4 id="negatives-6">Negatives</h4>
<ul>
<li>Fewer features than alternatives</li>
<li>Small community and ecosystem</li>
<li>Not yet at a stable release</li>
<li>Different OS browsers can (and will) behave differently - extra testing required</li>
</ul>
<h2 id="gui-library-overview">GUI Library Overview</h2>
<p>I thought it would be beneficial to have a casual overview of the differences between platforms, including differences in community size and support.</p>
<p>Releases in the past 6 months gives some indication of activity on each project, and includes beta, dev, and RC releases. This information is taken from each project’s git repository and is checked between 1st July 2021 and 1st January 2022.</p>
<p>As SwiftUI is not open source and other than at WWDC where major changes are announced, we do not get a run-down of changes between Xcode versions, it’s difficult to compare. We do know however that SwiftUI is backed by Apple and appears to be the recommended way of making apps for the Apple ecosystem moving forward.</p>
<p>SwiftUI is also the only platform out of the list that does not support Windows/Linux. It does however have support for iOS, iPadOS, Apple Watch, and Apple TV. If you’re in the Apple ecosystem, it’s definitely something to consider.</p>
<table><thead><tr><th>Framework/Library</th><th>Language(s)</th><th>Native</th><th>Platform Support</th><th>Contributors</th><th>Releases in past 6 months</th><th>Initial release date</th><th>Stable release?</th></tr></thead><tbody>
<tr><td>Compose</td><td>Kotlin</td><td>✅</td><td>💻🪟🐧🤖</td><td>64</td><td>51</td><td>2nd April 2021</td><td>✅</td></tr>
<tr><td>egui</td><td>Rust</td><td>✅</td><td>💻🪟🐧</td><td>89</td><td>4</td><td>30th May 2020</td><td>❌</td></tr>
<tr><td>Electron</td><td>Javascript</td><td>❌</td><td>💻🪟🐧</td><td>1081</td><td>113</td><td>12 Aug 2013</td><td>✅</td></tr>
<tr><td>React Native for Windows</td><td>Javascript/Typescript</td><td>✅</td><td>💻🪟🤖📱</td><td>180</td><td>49</td><td>23 Jun 2020</td><td>❌</td></tr>
<tr><td>Flutter</td><td>Dart</td><td>✅</td><td>💻🪟🐧🤖📱</td><td>957</td><td>28</td><td>27th Feb 2018</td><td>✅</td></tr>
<tr><td>Tauri</td><td>Rust + Javascript</td><td>❌</td><td>💻🪟🐧</td><td>114</td><td>4</td><td>18th December 2019</td><td>❌</td></tr>
</tbody></table>
<h3 id="features">Features</h3>
<p>Not all frameworks have every feature. If you’re looking to make an application that relies on specific things such as webcam support then you’ll need to check if it works or you’ll have to code it yourself.</p>
<p>Note that, my Google-foo may fail. I have tried looking through documentation and various resources for each library but unfortunately it’s not always easy to find if a solution exists.</p>
<p>Additionally, these features may get added after this article is published, so do your own research, too!</p>
<p>Here’s a key for the tables below.</p>
<ul>
<li>✅ - native/first party support</li>
<li>📦 - support via external plugin</li>
<li>🎓 - tutorial/community information available</li>
<li>❓- Unknown (Most likely unavailable)</li>
<li>❌ - unsupported/unavailable</li>
</ul>
<p>For theming and light/dark mode I’ll be looking at native support for the Operating System’s features. Web wrappers also generally have features that you can use from the browser, e.g. webcam support via JS, which I mention in the table.</p>
<p>Automatic updates for Linux are only available for Electron and Tauri via AppImage. Unfortunately most libraries don’t support over the air updates or only partially support it, and in this case you’ll have to either implement it yourself, or simply prompt the user to install the next update manually by checking a web-hook that you set up and manage.</p>
<table><thead><tr><th>Framework/Library</th><th>Context Menus</th><th>Window Menus</th><th>Multiple Windows/Window Manipulation</th><th>Webcam/Microphone</th><th>Automatic updates</th><th>Theming, Light and Dark mode</th><th>Tray</th></tr></thead><tbody>
<tr><td>Compose</td><td>✅</td><td>✅</td><td>✅</td><td>❌</td><td>❌ (<a href="https://github.com/JetBrains/compose-jb/issues/1043">issue</a>)</td><td>🎓(<a href="https://dev.to/tkuenneth/automatically-switch-to-dark-mode-and-back-in-compose-for-desktop-303l">link</a>)</td><td>✅</td></tr>
<tr><td>egui</td><td>✅</td><td>✅ (<a href="https://docs.rs/egui/0.3.0/egui/menu/index.html">basic</a>)</td><td>❓(<a href="https://github.com/emilk/egui/issues/552">issue)</a></td><td>❌</td><td>❌</td><td>🎓(<a href="https://github.com/emilk/egui/issues/1001">link</a>)</td><td>❌</td></tr>
<tr><td>Electron</td><td>📦 (<a href="https://www.npmjs.com/package/electron-context-menu">plugin</a>)</td><td>✅</td><td>✅</td><td>✅ (Via JS)</td><td>💻🪟🐧</td><td>✅ (<a href="https://www.electronjs.org/docs/latest/tutorial/dark-mode">link</a>)</td><td>✅</td></tr>
<tr><td>Flutter</td><td>📦 (<a href="https://pub.dev/packages/flutter_menu">1</a>, <a href="https://bestofcpp.com/repo/lesnitsky-native_context_menu">2</a>)</td><td>📦 (p<a href="https://github.com/google/flutter-desktop-embedding/tree/master/plugins/menubar">lugin)</a></td><td>❌</td><td>❌</td><td>❌</td><td>❓</td><td>🎓(<a href="https://stackoverflow.com/a/69645100">link</a>)</td></tr>
<tr><td>React Native for Windows</td><td>❌</td><td>❓</td><td>❓</td><td>❓</td><td>Microsoft Store</td><td>✅</td><td>❌</td></tr>
<tr><td>SwiftUI</td><td>✅</td><td>✅</td><td>✅</td><td>✅ (Using AppKit)</td><td>Mac App Store, <a href="https://sparkle-project.org">Sparkle</a></td><td>✅</td><td>✅</td></tr>
<tr><td>Tauri</td><td>❌ (JS library work around)</td><td>✅</td><td>✅</td><td>(Via JS)</td><td>💻🪟🐧</td><td>✅ (Via CSS)</td><td>✅</td></tr>
</tbody></table>
<h3 id="accessibility">Accessibility</h3>
<p>There’s many different levels of accessibility so I thought it would be worth investigating.</p>
<p>When looking at font size, I’m referring to ability to use the Operating System’s font scaling. Most tools are able to implement their own font scaling if they wanted to — or with a bit of additional code.</p>
<p>Interestingly, I tried testing this with Compose on Windows and the font refused to scale up. egui and Flutter worked fine, and browser based libraries will use the web browsers native font scaling.</p>
<table><thead><tr><th>Framework/Library</th><th>Voice over</th><th>Keyboard shortcuts</th><th>Tooltips</th><th>OS font size scaling</th><th>Tab focusing/cycling</th></tr></thead><tbody>
<tr><td>Compose</td><td>✅ - Mac Only, Windows planned</td><td>✅</td><td>✅</td><td>❌</td><td>✅</td></tr>
<tr><td>egui</td><td>❌ (i<a href="https://github.com/emilk/egui/issues/167">ssue)</a></td><td>❌ (i<a href="https://github.com/emilk/egui/issues/31">ssue)</a></td><td>✅</td><td>✅</td><td>✅</td></tr>
<tr><td>Electron</td><td>✅</td><td>✅</td><td>🎓 (<a href="https://www.npmjs.com/package/electron-tooltip">link</a>)</td><td>✅ (Chromium handles this)</td><td>✅</td></tr>
<tr><td>Flutter</td><td>❓(<a href="https://docs.flutter.dev/development/accessibility-and-localization/accessibility">link</a>)</td><td>✅(link <a href="https://docs.flutter.dev/development/ui/advanced/actions_and_shortcuts">1</a>, <a href="https://wilsonwilson.dev/articles/keyboard-shortcuts-in-flutter/">2</a>)</td><td>✅</td><td>✅</td><td>🎓 (<a href="https://github.com/flutter/flutter/issues/55033">link</a>)</td></tr>
<tr><td>React Native for Windows</td><td>❓</td><td>❌ (<a href="https://github.com/microsoft/react-native-windows/issues/3450">issue</a>)</td><td>✅</td><td>✅</td><td>✅</td></tr>
<tr><td>SwiftUI</td><td>✅</td><td>✅</td><td>✅</td><td>✅</td><td>✅ (MacOS Montery+)</td></tr>
<tr><td>Tauri</td><td>❓</td><td>✅ (Via JS)</td><td>✅(Via JS)</td><td>✅</td><td>✅</td></tr>
</tbody></table>
<h3 id="final-recommendations">Final Recommendations</h3>
<p>When choosing a library for building a desktop app, I think you have to ask yourself which category your application falls into:</p>
<ol>
<li>Personal project to solve your own issue</li>
<li>Small scope software with few updates or released as feature complete</li>
<li>Projects targeting developers</li>
<li>Product to be distributed and available to as many people as possible with frequent updates (E.g. a SaaS)</li>
<li>Enterprise - stability and maintainability at utmost importance</li>
</ol>
<p>For personal and feature complete software, I’d suggest going for the one that appeals the most to you, assuming it has the features you need.</p>
<p>For most other projects, you're most likely going to want to have automatic updates available. That is, unless you want to respond to every support request with 'Can you update to the latest version please’.</p>
<p>It's a real shame that it removes many of the otherwise great libraries from the running. If you can get away with it, you could instead implement a prompt that tells users to download a newer version manually when it’s available. Still, OTA updates are almost a requirement for desktop software today.</p>
<p>There is also a niche for software that only targets Apple devices. Plenty of developers go this route, just take a look at <a href="https://www.sketch.com">Sketch</a>, <a href="http://panic.com">Panic</a>, <a href="http://craft.do">Craft docs</a>, as a few examples. It certainly simplifies development, and if you're already in the Apple ecosystem it's great to scratch your own itch. If this sounds like your situation then SwiftUI is a great choice.</p>
<p>I really like all of these libraries, but Electron is the solution that's least likely to bite you with it's large community, ecosystem and feature set. That said, I'm eager to see the other solution grow in the future.</p>
<p>If you have any thoughts or suggestions for tools I should check out. Please feel free to comment! You can reach me on <a href="https://mastodon.technology/@mitchartemis/107656532224254703">Mastadon</a>, <a href="https://twitter.com/mitchartemis_v2/status/1484252370192187392">Twitter</a>, <a href="https://dev.to/mitchartemis/desktop-guis-for-web-developers-2i9d">Dev.to</a>, <a href="https://mitchartemis.dev/2022/01/20/new-article-this.html">Micro.blog</a>, or comment directly on the original article.</p>
Building small desktop apps with Ember.js and Tauri2021-05-23T00:00:00+00:002021-05-23T00:00:00+00:00https://www.fullstackstanley.com/articles/building-tiny-desktop-apps-with-ember-and-tauri/<p>I recently played around with Tauri, a toolkit for development desktop apps with web technologies. Here's how I got it working with an Ember.js application.</p>
<span id="continue-reading"></span><h2 id="what-is-ember">What is Ember?</h2>
<p>Ember.js is a frontend framework similar to React and Vue JS. I used it to build my app <a href="https://snipline.io">Snipline</a>, and it's also used for websites like Intercom and LinkedIn. It has a 'convention over configuration' approach similar to Ruby on Rails.</p>
<h2 id="what-is-tauri">What is Tauri?</h2>
<p>Tauri is a library for making desktop applications with web technologies. Similar to Electron with a few key differences: </p>
<ol>
<li>
<p>It's built in Rust rather than Javascript. </p>
</li>
<li>
<p>It uses your operating system's native web browser rather than bundling Chrome which resulting in quite tiny applications—at least compared to Electron!</p>
</li>
</ol>
<h2 id="installation-and-development">Installation and development</h2>
<p>Here are the commands I ran for a simple Ember app to test routing with Ember and Tauri. For reference, I'm using Node. 14.17.0.</p>
<h3 id="setting-up-ember">Setting up Ember</h3>
<pre data-lang="bash" style="background-color:#2b303b;color:#c0c5ce;" class="language-bash "><code class="language-bash" data-lang="bash"><span style="color:#bf616a;">npm</span><span> install</span><span style="color:#bf616a;"> -g</span><span> ember-cli
</span></code></pre>
<pre data-lang="bash" style="background-color:#2b303b;color:#c0c5ce;" class="language-bash "><code class="language-bash" data-lang="bash"><span style="color:#bf616a;">ember</span><span> new tauri-test</span><span style="color:#bf616a;"> --lang</span><span> en
</span></code></pre>
<pre data-lang="bash" style="background-color:#2b303b;color:#c0c5ce;" class="language-bash "><code class="language-bash" data-lang="bash"><span style="color:#bf616a;">ember</span><span> g route index
</span><span style="color:#bf616a;">ember</span><span> g route from-ember
</span></code></pre>
<pre style="background-color:#2b303b;color:#c0c5ce;"><code><span>ember serve
</span></code></pre>
<p>I edited the two generated templates, <code>app/templates/index.hbs</code> and <code>app/templates/from-ember.hbs</code>.</p>
<pre data-lang="js" style="background-color:#2b303b;color:#c0c5ce;" class="language-js "><code class="language-js" data-lang="js"><span>{{</span><span style="color:#bf616a;">page</span><span>-</span><span style="color:#bf616a;">title </span><span>"</span><span style="color:#a3be8c;">Index</span><span>"}}
</span><span><h1></span><span style="color:#bf616a;">Hello</span><span>, </span><span style="color:#bf616a;">Tauri</span><span> 😄</</span><span style="color:#bf616a;">h1</span><span>>
</span><span><</span><span style="color:#bf616a;">LinkTo </span><span>@</span><span style="color:#bf616a;">route</span><span>="</span><span style="color:#a3be8c;">from-ember</span><span>"></span><span style="color:#bf616a;">Click here</span><span></</span><span style="color:#bf616a;">LinkTo</span><span>>
</span></code></pre>
<pre data-lang="js" style="background-color:#2b303b;color:#c0c5ce;" class="language-js "><code class="language-js" data-lang="js"><span>{{</span><span style="color:#bf616a;">page</span><span>-</span><span style="color:#bf616a;">title </span><span>"</span><span style="color:#a3be8c;">FromEmber</span><span>"}}
</span><span><h1></span><span style="color:#bf616a;">From Ember</span><span> 🧡</</span><span style="color:#bf616a;">h1</span><span>>
</span><span><</span><span style="color:#bf616a;">LinkTo </span><span>@</span><span style="color:#bf616a;">route</span><span>="</span><span style="color:#a3be8c;">index</span><span>"></span><span style="color:#bf616a;">Back</span><span></</span><span style="color:#bf616a;">LinkTo</span><span>>
</span></code></pre>
<p>That's enough to get started and test that routing works within the app. Now let's get to the good stuff.</p>
<h3 id="setting-up-tauri">Setting up Tauri</h3>
<p>First, <a href="https://tauri.studio/en/docs/getting-started/intro">follow the set up guide for your OS in the Tauri documentation</a>.</p>
<p>After this, it's a matter of adding it to your ember project - <a href="https://tauri.studio/en/docs/usage/development/integration">See the integration documentation</a>.</p>
<p>This is what I did to get it working.</p>
<pre data-lang="bash" style="background-color:#2b303b;color:#c0c5ce;" class="language-bash "><code class="language-bash" data-lang="bash"><span style="color:#bf616a;">npm</span><span> install @tauri-apps/cli
</span></code></pre>
<pre style="background-color:#2b303b;color:#c0c5ce;"><code><span>// Add the `tauri` command to your `package.json`
</span><span>{
</span><span> // This content is just a sample
</span><span> "scripts": {
</span><span> "tauri": "tauri"
</span><span> }
</span><span>}
</span></code></pre>
<p>Run through the initialisation process.</p>
<pre data-lang="bash" style="background-color:#2b303b;color:#c0c5ce;" class="language-bash "><code class="language-bash" data-lang="bash"><span style="color:#bf616a;">npm</span><span> run tauri init
</span></code></pre>
<p>When prompted, make sure that you set the development server to <code>http://localhost:4200</code> and the location of the files (relative to <code>src-tauri</code>) to <code>../dist</code>.</p>
<p>Then it's just a matter of running the development subcommand (Make sure your Ember server is still up, too).</p>
<pre style="background-color:#2b303b;color:#c0c5ce;"><code><span>npm run tauri dev
</span></code></pre>
<p>And that's it! It works even with hot reloading!</p>
<h2 id="packaging">Packaging</h2>
<p>With development out of the way, here's how to package the app for distribution. I won't be looking at auto-updates in this guide, but <a href="https://tauri.studio/en/docs/usage/guides/updater">Tauri does have support for this</a>.</p>
<pre data-lang="bash" style="background-color:#2b303b;color:#c0c5ce;" class="language-bash "><code class="language-bash" data-lang="bash"><span style="color:#bf616a;">ember</span><span> build</span><span style="color:#bf616a;"> --environment</span><span>=production
</span><span style="color:#bf616a;">npm</span><span> run tauri build
</span></code></pre>
<p>On MacOS installer <code>.dmg</code> file came out at 5.4MB and the <code>.app</code> file 12.4MB.</p>
<figure class="figure ">
<img src="/images/tauri-ember.gif"
alt="Tauri running the Ember.js app in MacOS">
<figcaption>Tauri running the Ember.js app in MacOS</figcaption>
</figure>
<p>For Windows, the generated MSI installer came to 4.9MB and the executable 8.9MB.</p>
<figure class="figure ">
<img src="/images/tauri-ember.png"
alt="Tauri running the Ember.js app in Windows">
<figcaption>Tauri running the Ember.js app in Windows</figcaption>
</figure>
<h2 id="communicating-between-rust-and-ember">Communicating between Rust and Ember</h2>
<p>Taking this one step further, I thought I'd test a simple ping/pong example of communicating between Ember and Rust. For more information <a href="https://tauri.studio/en/docs/usage/guides/command">check the Tauri docs</a>.</p>
<p>The following code allows for Ember to pass a string to Rust, Rust checks the value and toggles between the text 'Ping' and 'Pong'. In Ember, I've added a button that displays the response text and updates it on click.</p>
<pre data-lang="rust" style="background-color:#2b303b;color:#c0c5ce;" class="language-rust "><code class="language-rust" data-lang="rust"><span style="color:#65737e;">// src-tauri/src/main.rs
</span><span>#![</span><span style="color:#bf616a;">cfg_attr</span><span>(
</span><span> </span><span style="color:#bf616a;">all</span><span>(</span><span style="color:#bf616a;">not</span><span>(debug_assertions), target_os = "</span><span style="color:#a3be8c;">windows</span><span>"),
</span><span> windows_subsystem = "</span><span style="color:#a3be8c;">windows</span><span>"
</span><span>)]
</span><span>
</span><span style="color:#65737e;">// Add a new function that takes a string and returns a string
</span><span>#[</span><span style="color:#bf616a;">tauri</span><span>::</span><span style="color:#bf616a;">command</span><span>]
</span><span style="color:#b48ead;">fn </span><span style="color:#8fa1b3;">my_custom_command</span><span>(</span><span style="color:#bf616a;">current_text</span><span>: String) -> String {
</span><span> </span><span style="color:#65737e;">// Depending on what we receive from Ember we toggle the response
</span><span> </span><span style="color:#b48ead;">if</span><span> current_text == "</span><span style="color:#a3be8c;">Ping</span><span>" {
</span><span> "</span><span style="color:#a3be8c;">Pong!</span><span>".</span><span style="color:#96b5b4;">into</span><span>()
</span><span> } </span><span style="color:#b48ead;">else </span><span>{
</span><span> "</span><span style="color:#a3be8c;">Ping</span><span>".</span><span style="color:#96b5b4;">into</span><span>()
</span><span> }
</span><span>}
</span><span>
</span><span style="color:#b48ead;">fn </span><span style="color:#8fa1b3;">main</span><span>() {
</span><span> </span><span style="color:#65737e;">// Add the custom command so that the frontend can invoke it
</span><span> tauri::Builder::default()
</span><span> .</span><span style="color:#96b5b4;">invoke_handler</span><span>(tauri::generate_handler![my_custom_command])
</span><span> .</span><span style="color:#96b5b4;">run</span><span>(tauri::generate_context!())
</span><span> .</span><span style="color:#96b5b4;">expect</span><span>("</span><span style="color:#a3be8c;">error while running tauri application</span><span>");
</span><span>}
</span></code></pre>
<pre data-lang="js" style="background-color:#2b303b;color:#c0c5ce;" class="language-js "><code class="language-js" data-lang="js"><span style="color:#65737e;">// app/controllers/index.js
</span><span style="color:#b48ead;">import </span><span style="color:#bf616a;">Controller </span><span style="color:#b48ead;">from </span><span>'</span><span style="color:#a3be8c;">@ember/controller</span><span>'
</span><span style="color:#b48ead;">import </span><span>{ </span><span style="color:#bf616a;">action </span><span>} </span><span style="color:#b48ead;">from </span><span>'</span><span style="color:#a3be8c;">@ember/object</span><span>'
</span><span style="color:#b48ead;">import </span><span>{ </span><span style="color:#bf616a;">tracked </span><span>} </span><span style="color:#b48ead;">from </span><span>'</span><span style="color:#a3be8c;">@glimmer/tracking</span><span>'
</span><span style="color:#b48ead;">import </span><span>{ </span><span style="color:#bf616a;">invoke </span><span>} </span><span style="color:#b48ead;">from </span><span>'</span><span style="color:#a3be8c;">@tauri-apps/api/tauri</span><span>'
</span><span>
</span><span style="color:#b48ead;">export default class </span><span style="color:#ebcb8b;">IndexController </span><span style="color:#b48ead;">extends </span><span style="color:#a3be8c;">Controller </span><span style="color:#eff1f5;">{
</span><span style="color:#eff1f5;"> </span><span style="color:#65737e;">// Set the default button text
</span><span style="color:#eff1f5;"> @</span><span style="color:#bf616a;">tracked buttonText </span><span>= '</span><span style="color:#a3be8c;">Ping</span><span>'
</span><span style="color:#eff1f5;"> </span><span style="color:#65737e;">// Create an action that will be attached to a button in the template
</span><span style="color:#eff1f5;"> @</span><span style="color:#bf616a;">action
</span><span style="color:#eff1f5;"> </span><span style="color:#8fa1b3;">checkRust</span><span>() </span><span style="color:#eff1f5;">{
</span><span style="color:#eff1f5;"> </span><span style="color:#65737e;">// Invoke the Rust command and update the button text to the response
</span><span style="color:#eff1f5;"> </span><span style="color:#8fa1b3;">invoke</span><span style="color:#eff1f5;">(</span><span>'</span><span style="color:#a3be8c;">my_custom_command</span><span>'</span><span style="color:#eff1f5;">, { currentText: </span><span style="color:#bf616a;">this</span><span style="color:#eff1f5;">.</span><span style="color:#bf616a;">buttonText </span><span style="color:#eff1f5;">}).</span><span style="color:#96b5b4;">then</span><span style="color:#eff1f5;">(</span><span style="color:#bf616a;">resp </span><span style="color:#b48ead;">=> </span><span style="color:#eff1f5;">{
</span><span style="color:#eff1f5;"> </span><span style="color:#ebcb8b;">console</span><span style="color:#eff1f5;">.</span><span style="color:#96b5b4;">log</span><span style="color:#eff1f5;">(</span><span style="color:#bf616a;">resp</span><span style="color:#eff1f5;">)
</span><span style="color:#eff1f5;"> </span><span style="color:#bf616a;">this</span><span style="color:#eff1f5;">.</span><span style="color:#bf616a;">buttonText </span><span>= </span><span style="color:#bf616a;">resp
</span><span style="color:#eff1f5;"> })
</span><span style="color:#eff1f5;"> }
</span><span style="color:#eff1f5;">}
</span></code></pre>
<p>Here's the updated <code>app/templates/index.hbs</code> template file.</p>
<pre data-lang="js" style="background-color:#2b303b;color:#c0c5ce;" class="language-js "><code class="language-js" data-lang="js"><span>{{</span><span style="color:#bf616a;">page</span><span>-</span><span style="color:#bf616a;">title </span><span>"</span><span style="color:#a3be8c;">Index</span><span>"}}
</span><span><h1></span><span style="color:#bf616a;">Hello</span><span>, </span><span style="color:#bf616a;">Tauri</span><span> 😄</</span><span style="color:#bf616a;">h1</span><span>>
</span><span><</span><span style="color:#bf616a;">LinkTo </span><span>@</span><span style="color:#bf616a;">route</span><span>="</span><span style="color:#a3be8c;">from-ember</span><span>"></span><span style="color:#bf616a;">Click here</span><span></</span><span style="color:#bf616a;">LinkTo</span><span>>
</span><span><</span><span style="color:#bf616a;">button </span><span>{{ </span><span style="color:#bf616a;">on </span><span>'</span><span style="color:#a3be8c;">click</span><span>' </span><span style="color:#bf616a;">this</span><span>.</span><span style="color:#bf616a;">checkRust </span><span>}}>{{this.</span><span style="color:#bf616a;">buttonText</span><span>}}</</span><span style="color:#bf616a;">button</span><span>>
</span></code></pre>
<figure class="figure ">
<img src="/images/ember-tauri2.gif"
alt="Tauri and Ember.js communicating with each other">
<figcaption>Tauri and Ember.js communicating with each other</figcaption>
</figure>
<p>Pretty cool! I'm excited to see where Tauri goes, and to see its plugin ecosystem grow. Should I try building a full project in it or write some more tutorials using both technologies? Let me know in the comments!</p>
Making an Electron App with Ember JS Part #4: Windows2020-02-20T00:00:00+00:002020-02-20T00:00:00+00:00https://www.fullstackstanley.com/articles/making-an-electron-app-with-ember-js-part-4-windows/<p>This is part four in the series of blog posts “Making an Electron App with Ember JS” where I go over how I built my app <a href="https://snipline.io">Snipline</a> for the web, Mac, Windows, and Linux.</p>
<span id="continue-reading"></span>
<p>This post assumes you’ve read the previous chapters, if you haven’t then I highly recommend you do.</p>
<h2 id="notes-for-building-electron-apps-for-windows">Notes for building Electron apps for Windows</h2>
<p>To build the app for Windows you will need access to a machine that runs Windows. </p>
<p>We'll be building the app for external distribution (Downloadable from the web). It’s possible to build for the Windows Store but I have not had experience doing this. Please leave a comment below if you’ve done this!</p>
<p>As with MacOS, it’s highly recommended that you code sign your releases. Without doing this users will see warnings when trying to install your application. I use Sectigo for my certificates which start at $166/year. I’ll go into more details on this process throughout the rest of the article.</p>
<h2 id="certificate-setup">Certificate Setup</h2>
<p>As mentioned, I use Sectigo for my code signing certificates which you can find <a href="https://sectigo.com/signing-certificates/code-signing">here</a>. After you’ve purchased a certificate you may have to wait a few days for it to be sent to you.</p>
<p>You’ll receive an email with a link to install the certificate. Make sure that you click this on the Windows machine that you’ll be using in Internet Explorer - <em>NOT</em> Microsoft Edge.</p>
<p>Once you have installed the certificate you’ll need to export it to a <code>.pfx</code> file for use with the Electron build process. To do this, <a href="https://support.sectigo.com/Com_KnowledgeDetailPage?Id=kA01N000000zFK0#ie_export_certificate">follow this guide from Sectigo</a>.</p>
<p>Make sure to give the certificate a strong password!</p>
<h2 id="configuring-the-app-to-build-for-windows">Configuring the app to build for Windows</h2>
<p>Once you have the Ember app set up on your Windows machine you’ll need to make a few tweaks to the <code>ember-electron/electron-forge-config.js</code> file.</p>
<p>First, add a function for getting the code signing password you used earlier.</p>
<pre data-lang="javascript" style="background-color:#2b303b;color:#c0c5ce;" class="language-javascript "><code class="language-javascript" data-lang="javascript"><span style="color:#b48ead;">function </span><span style="color:#8fa1b3;">getSigningPassword</span><span>() {
</span><span> </span><span style="color:#b48ead;">if </span><span>(process.platform !== '</span><span style="color:#a3be8c;">win32</span><span>') {
</span><span> </span><span style="color:#b48ead;">return</span><span>;
</span><span> }
</span><span>
</span><span> </span><span style="color:#b48ead;">if </span><span>(process.env.</span><span style="color:#bf616a;">CODESIGN_PASSWORD</span><span>) {
</span><span> </span><span style="color:#b48ead;">return </span><span>process.env.</span><span style="color:#bf616a;">CODESIGN_PASSWORD</span><span>;
</span><span> } </span><span style="color:#b48ead;">else </span><span>{
</span><span> </span><span style="color:#ebcb8b;">console</span><span>.</span><span style="color:#96b5b4;">log</span><span>('</span><span style="color:#a3be8c;">Codesigning password can not be found, release build will fail</span><span>');
</span><span> </span><span style="color:#ebcb8b;">console</span><span>.</span><span style="color:#96b5b4;">log</span><span>('</span><span style="color:#a3be8c;">To fix, set CODESIGN_PASSWORD</span><span>');
</span><span> }
</span><span>}
</span></code></pre>
<p>Add or update the <code>electronWinstallerConfig</code> object</p>
<pre style="background-color:#2b303b;color:#c0c5ce;"><code><span> // ...
</span><span> "electronWinstallerConfig": {
</span><span> "name": "acme",
</span><span> "noMsi": true,
</span><span> "authors": 'acme',
</span><span> "exe": 'Shopper.exe',
</span><span> "title": "Shopper",
</span><span> "certificateFile": "<certificate location>",
</span><span> "certificatePassword": getSigningPassword(),
</span><span> "icon": path.join(rootPath, 'electron-assets', 'shopper.ico'),
</span><span> },
</span><span> // ...
</span></code></pre>
<p>There are a few values you will need to update: <code>name</code>, <code>authors</code>, <code>exe</code>, <code>title</code>, <code>certificateFile</code>, and <code>icon</code>.</p>
<p>The <code>ico</code> file needs to be a <code>256x256</code> icon. This will be what is used on Windows as your app icon.</p>
<p>The <code>certificateFile</code> needs to be updated to the location where you saved the exported certificate.</p>
<p>To build the application use the following command, replacing the password with your own.</p>
<pre data-lang="bash" style="background-color:#2b303b;color:#c0c5ce;" class="language-bash "><code class="language-bash" data-lang="bash"><span style="color:#bf616a;">env</span><span> CODESIGN_PASSWORD=<codesign password> env ELECTRON_ENV=production ember electron:make</span><span style="color:#bf616a;"> --environment</span><span>=production
</span></code></pre>
<p>One thing to mention is that this command is temperamental on Windows. I’ve had several builds fail due to code signing failures only for it to work with no changes later on. If you do receive this error, make sure to keep trying a few more times.</p>
<h2 id="final-notes">Final Notes</h2>
<p>And there you have it! You now know how to build an Ember JS Electron application across three of the most popular desktop platforms.</p>
<p>There are several things that can be improved on and investigated, including working with more Linux environments, integrating release upgrades for Windows and MacOS, or using Electron without relying on Ember Electron. These are all topics for other articles, but after reading through this series you should have enough to get your started. </p>
<p>Enjoy!</p>
Making an Electron App with Ember JS Part #2.5: MacOS Notarisation2019-08-23T00:00:00+00:002019-08-23T00:00:00+00:00https://www.fullstackstanley.com/articles/making-an-electron-app-with-ember-js-part-2-5-macos-notarisation/<p>This is a quick, cheeky part two-point-five of the blog post series “Making an Electron App with Ember JS” where I go over how I built my app <a href="https://snipline.io">Snipline</a> for the web, Mac, Windows, and Linux.</p>
<span id="continue-reading"></span>
<p>This is a quick, cheeky part two-point-five of the blog post series “Making an Electron App with Ember JS” where I go over how I built my app <a href="https://snipline.io">Snipline</a> for the web, Mac, Windows, and Linux.</p>
<p>With the upcoming update to MacOS, 10.15 Catalina, it’s important to notarise your app or your users will not be able to install it! I did not go over this in the previous post but felt it needed addressing.</p>
<h2 id="what-is-notarisation">What is notarisation?</h2>
<p>Notarisation is a new feature to the MacOS ecosystem which is required for apps distributed outside of the App Store. This feature allows Apple to scan your software for malicious content so that users can be confident that your app is safe to use. Not only this, but if your signing key is exposed, you can contact Apple to disable unauthorised versions of your app from being opened.</p>
<p>You can read more about this on the <a href="https://developer.apple.com/documentation/security/notarizing_your_app_before_distribution">Apple Developer website</a>.</p>
<h2 id="notarising-our-app">Notarising our app</h2>
<p>There a few changes that need to be made to notarise our app.</p>
<p>First, we need add <code>electron-notorize</code> to the <code>package.json</code></p>
<pre data-lang="diff" style="background-color:#2b303b;color:#c0c5ce;" class="language-diff "><code class="language-diff" data-lang="diff"><span style="color:#a3be8c;">+ "electron-notarize": "^0.1.1"
</span></code></pre>
<p>Next, in our <code>ember-electron/electron-forge.config.js</code> file we need to add the following code changes.</p>
<pre data-lang="diff" style="background-color:#2b303b;color:#c0c5ce;" class="language-diff "><code class="language-diff" data-lang="diff"><span style="color:#a3be8c;">+ const { notarize } = require('electron-notarize');
</span><span>// ...
</span><span>"osxSign": {
</span><span style="color:#bf616a;">- "identity": getCodesignIdentity()
</span><span style="color:#a3be8c;">+ "identity": getCodesignIdentity(),
</span><span style="color:#a3be8c;">+ "gatekeeper-assess": false,
</span><span style="color:#a3be8c;">+ "hardened-runtime": true,
</span><span style="color:#a3be8c;">+ "entitlements": path.join(rootPath, "ember-electron", "resources-darwin", "entitlements.plist"),
</span><span style="color:#a3be8c;">+ "entitlements-inherit": path.join(rootPath, "ember-electron", "resources-darwin", "entitlements.plist")
</span><span>},
</span><span>// ...
</span><span style="color:#a3be8c;">+ "hooks": {
</span><span style="color:#a3be8c;">+ postPackage: async function () {
</span><span style="color:#a3be8c;">+ if (process.platform !== 'darwin') {
</span><span style="color:#a3be8c;">+ console.log('Skipping notarization - not building for Mac');
</span><span style="color:#a3be8c;">+ return;
</span><span style="color:#a3be8c;">+ }
</span><span style="color:#a3be8c;">+
</span><span style="color:#a3be8c;">+ console.log('Notarizing...');
</span><span style="color:#a3be8c;">+
</span><span style="color:#a3be8c;">+ await notarize({
</span><span style="color:#a3be8c;">+ appBundleId: getBundleId(),
</span><span style="color:#a3be8c;">+ appPath: path.join(rootPath, "electron-out", "Shopper-darwin-x64", "Shopper.app"),
</span><span style="color:#a3be8c;">+ appleId: process.env.APPLE_ID,
</span><span style="color:#a3be8c;">+ appleIdPassword: process.env.APPLE_ID_PASSWORD
</span><span style="color:#a3be8c;">+ });
</span><span style="color:#a3be8c;">+ }
</span></code></pre>
<p>What do these changes do? Firstly, gatekeeper needs to be disabled for this to work correctly, and we need to specify an <code>entitlements.plist</code> file which we’ll create next. The <code>postPackage</code> hook deals with the notarisation. We check if we're compiling for MacOS, and then run the notarisation process. </p>
<p>You will need to change the <code>Shopper</code> references to your own app name. </p>
<p>We’re also specifying two new environment variables that will need to be passed to the build command, <code>APPLE_ID</code> and <code>APPLE_ID_PASSWORD</code>. This password is app specific and can be generated from your account at <a href="http://developer.apple.com">https://appleid.apple.com </a> - <strong>do not use your real Apple ID password here!</strong>. See these instructions for more details <a href="https://support.apple.com/en-us/HT204397">https://support.apple.com/en-us/HT204397</a></p>
<p>Next, it’s time to create the entitlements file, create <code>ember-electron/resources-darwin/entitlements.plist</code> and add the following</p>
<pre data-lang="bash" style="background-color:#2b303b;color:#c0c5ce;" class="language-bash "><code class="language-bash" data-lang="bash"><span><?xml </span><span style="color:#bf616a;">version</span><span>="</span><span style="color:#a3be8c;">1.0</span><span>" </span><span style="color:#bf616a;">encoding</span><span>="</span><span style="color:#a3be8c;">UTF-8</span><span>"?>
</span><span><!DOCTYPE </span><span style="color:#bf616a;">plist</span><span> PUBLIC "</span><span style="color:#a3be8c;">-//Apple//DTD PLIST 1.0//EN</span><span>" "</span><span style="color:#a3be8c;">http://www.apple.com/DTDs/PropertyList-1.0.dtd</span><span>">
</span><span><plist </span><span style="color:#bf616a;">version</span><span>="</span><span style="color:#a3be8c;">1.0</span><span>">
</span><span> <dict>
</span><span> <key>com.apple.security.cs.allow-unsigned-executable-memory</key>
</span><span> <true/>
</span><span> </dict>
</span><span></plist>
</span></code></pre>
<p>Now we can build the new release. Note that this may take some time, as the app gets checked on Apple’s servers. Remember to change the environment variables to your own.</p>
<pre data-lang="bash" style="background-color:#2b303b;color:#c0c5ce;" class="language-bash "><code class="language-bash" data-lang="bash"><span style="color:#bf616a;">env</span><span> CODESIGN_IDENTITY="</span><span style="color:#a3be8c;">Developer ID Application: <Name> (<ID>)</span><span>" env BUNDLE_ID="</span><span style="color:#a3be8c;">io.exampledomain.desktop</span><span>" ELECTRON_ENV=production env APPLE_ID_PASSWORD=<password> env APPLE_ID=<appleid> ember electron:make</span><span style="color:#bf616a;"> --environment</span><span>=production
</span></code></pre>
<h2 id="the-dmg-file">The dmg file</h2>
<p>Code signing is no longer needed for <code>.dmg</code> files as Apple now checks the <code>.app</code> file within it. With this in mind, we can no longer use the <code>create-dmg</code> Javascript package as it automatically finds your certificate and applies it to the <code>.dmg</code> build.</p>
<p>Instead, we can use <code>electron-installer-dmg</code> which is already in our dependencies.</p>
<pre data-lang="bash" style="background-color:#2b303b;color:#c0c5ce;" class="language-bash "><code class="language-bash" data-lang="bash"><span style="color:#65737e;"># Unzip the generated app
</span><span style="color:#bf616a;">unzip</span><span> Shopper-darwin-x64-x.x.x.zip
</span><span style="color:#65737e;"># Generate the dmg installer
</span><span style="color:#bf616a;">./node_modules/.bin/electron-installer-dmg</span><span> ./electron-out/make/Shopper.app Shopper</span><span style="color:#bf616a;"> --out</span><span>=./electron-out/make/</span><span style="color:#bf616a;"> --icon</span><span>=./electron-assets/shopper.icns</span><span style="color:#bf616a;"> --icon-size</span><span>=100
</span></code></pre>
<p>That’s all there is to it!</p>
<p>In the next chapter we’ll take a look at building for Linux!</p>
Making an Electron App with Ember JS Part #3: Linux2019-07-11T00:00:00+00:002019-07-11T00:00:00+00:00https://www.fullstackstanley.com/articles/making-an-electron-app-with-ember-js-part-3-linux/<p>This is part three in the series of blog posts “Making an Electron App with Ember JS” where I go over how I built my app <a href="https://snipline.io">Snipline</a> for the web, Mac, Windows, and Linux.</p>
<span id="continue-reading"></span>
<p>This post assumes you’ve read the previous chapters, if you haven’t then I highly recommend you do.</p>
<h2 id="building-for-linux">Building for Linux</h2>
<p>Building for Linux can be tricky. You have to take into account the following:</p>
<ul>
<li>What distributions do you want to support?</li>
<li>What deployment channels do you wish to use?</li>
</ul>
<p>Unfortunately it's not possible to go through every single combination. In this post we'll be focusing on building a <code>.deb</code> package for Debian based distros such as Ubuntu which can be downloaded and installed via the web.</p>
<h2 id="building-for-debian">Building for Debian</h2>
<p>A debian package is a file that can be easily downloaded from the internet, it's very easy to set up and it's relatively easy for users to install. The downside is that to update the users will need to download the latest package manually. </p>
<p>The upside of building <code>.deb</code> packages is that you can do this from any Debian based distro or even from MacOS with a couple of dependencies installed.</p>
<h3 id="requirements">Requirements</h3>
<p>Unlike the MacOS or Windows builds, you don't <em>need</em> to use Debian to build for Debian. If you're on MacOS you need to install the following dependencies via Homebrew.</p>
<pre data-lang="bash" style="background-color:#2b303b;color:#c0c5ce;" class="language-bash "><code class="language-bash" data-lang="bash"><span style="color:#bf616a;">brew</span><span> install fakeroot dpkg
</span></code></pre>
<p>Update your <code>ember-electron-forge.js</code> to fill in the <code>electronInstallerDebian</code> section. Make sure to include your own applications information.</p>
<pre data-lang="diff" style="background-color:#2b303b;color:#c0c5ce;" class="language-diff "><code class="language-diff" data-lang="diff"><span> "make_targets": {
</span><span> // ...
</span><span> "linux": [
</span><span style="color:#a3be8c;">+ "deb",
</span><span> ]
</span><span> },
</span><span> // ...
</span><span> "electronInstallerDebian": {
</span><span style="color:#a3be8c;">+ "name": "shopper",
</span><span style="color:#a3be8c;">+ "productName": "Shopper",
</span><span style="color:#a3be8c;">+ "description": "A shopping list application",
</span><span style="color:#a3be8c;">+ "productDescription": "A shopping list application",
</span><span style="color:#a3be8c;">+ "icon": "electron-assets/shopper.png",
</span><span style="color:#a3be8c;">+ "bin": 'Shopper',
</span><span style="color:#a3be8c;">+ "desktopTemplate": path.join(rootPath, "ember-electron", "resources-linux", "desktop.ejs"),
</span><span style="color:#a3be8c;">+ "categories": [
</span><span style="color:#a3be8c;">+ "Utility"
</span><span style="color:#a3be8c;">+ ],
</span><span style="color:#a3be8c;">+ "homepage": "https://github.com/snipline/shopper"
</span><span> },
</span><span> // ...
</span></code></pre>
<p>Note that capitalisation is important in this file. See how <code>name</code> is lower case whereas <code>productName</code> and <code>bin</code> are uppercase.</p>
<p>For a list of valid categories <a href="https://standards.freedesktop.org/menu-spec/menu-spec-1.0.html#category-registry">see this page</a>.</p>
<p>If you've been paying attention to the code, you'll see that we need to make a "Desktop Template" (<code>.ejs.</code>) file in <code>ember-electron/resources-linux/desktop.ejs</code>. This is the contents of that file</p>
<pre data-lang="js" style="background-color:#2b303b;color:#c0c5ce;" class="language-js "><code class="language-js" data-lang="js"><span style="color:#65737e;">// ember-electron/resources-linux/desktop.ejs
</span><span>[</span><span style="color:#bf616a;">Desktop </span><span style="color:#ebcb8b;">Entry</span><span>]
</span><span><% </span><span style="color:#b48ead;">if </span><span>(</span><span style="color:#bf616a;">productName</span><span>) { %></span><span style="color:#bf616a;">Name</span><span>=<%= productName %>
</span><span><% } %><% if (description) { %></span><span style="color:#bf616a;">Comment</span><span>=<%= description %>
</span><span><% } %><% if (genericName) { %></span><span style="color:#bf616a;">GenericName</span><span>=<%= genericName %>
</span><span><% } %><% if (name) { %></span><span style="color:#bf616a;">Exec</span><span>=<%= name %> --</span><span style="color:#bf616a;">disable</span><span>-</span><span style="color:#bf616a;">gpu
</span><span style="color:#bf616a;">Icon</span><span>=<%= name %>
</span><span><% } %></span><span style="color:#bf616a;">Type</span><span>=</span><span style="color:#bf616a;">Application
</span><span style="color:#bf616a;">StartupNotify</span><span>=</span><span style="color:#d08770;">true
</span><span><% if (categories && categories.length) { %></span><span style="color:#bf616a;">Categories</span><span>=<%= categories.join('</span><span style="color:#a3be8c;">;</span><span>') %>;
</span><span><% } %><% if (mimeType && mimeType.length) { %></span><span style="color:#bf616a;">MimeType</span><span>=<%= mimeType.join('</span><span style="color:#a3be8c;">;</span><span>') %>;
</span><span><% } %>
</span></code></pre>
<p>The <code>desktop.ejs</code> file is the template for the application's <code>.desktop</code> file. This file takes the values we've just added to the Electron configuration and will add them to the generated Desktop file. The <code>.desktop</code> file then gets added to the user's local share directory when installed and makes the application available in the user's Applications menu.</p>
<p>Copy the icon (<code>shopper.png</code>) to <code>./electron-assets/shopper.png</code> (Or use your own icon) so that the build will have an icon - without this the build will fail.</p>
<p>If you're building this on MacOS, then before running the build command, we need to update any references to <code>process.platform</code> to <code>process.env.PLATFORM</code> in the Electron configuration file. This is because even though we're building for linux with the <code>--platform=linux</code> flag, Node still thinks the platform is <code>Darwin</code>.</p>
<p>Finally, to build for linux run the build command below.</p>
<pre style="background-color:#2b303b;color:#c0c5ce;"><code><span>env PLATFORM=linux env ELECTRON_ENV=production ember electron:make --environment=production --platform=linux
</span></code></pre>
<p>That's it! You should find a <code>.deb</code> file in your <code>make</code> directory.</p>
<h2 id="nativefier">Nativefier</h2>
<p>One final option for Linux, (and other Operating Systems if needed) is to use <a href="https://github.com/jiahaog/nativefier">Nativefier</a>. This essentially lets you wrap a web version of your app in Electron. The downside to this is the user needs internet access, you need to host the application online, and you get less control over other Electron settings. The upside is: it's very easy to implement and the user will always be on the latest version of your app.</p>
<h2 id="other-options">Other Options</h2>
<p>There are a lot of other options to build for Linux, including various package management systems such as apt, rpm and pacman. There's also Snapcraft, and Homebrew now supports Linux as well. Unfortunately, documentation for targeting these with Electron Forge is sparse. I'd encourage you to try them for yourself and see what you discover. Leave a comment if you get them working or even write an article! I'd love to see it.</p>
<p>In the next chapter we’ll take a look at building for Windows!</p>
Making an Electron App with Ember JS Part #1: Initial Setup2019-07-09T00:00:00+00:002019-07-09T00:00:00+00:00https://www.fullstackstanley.com/articles/making-an-electron-app-with-ember-js-part-1-initial-setup/<p>I work on the development of a tool called Snipline, a utility created to solve scratch my own itch, increasing my shell command productivity.</p>
<span id="continue-reading"></span>
<p>I first started building the web version of Snipline in Ember JS and soon discovered <a href="https://ember-electron.js.org">Ember Electron</a> which allowed me to create desktop clients of the web app for Windows, MacOS, and Linux really fast.</p>
<p>Although the addon does a great deal of work for you, there’s a lot of configuration that needs to be done as well - especially for releases. It’s taken me a long time as well as a lot of trial and error and wanted to share my discoveries with others.</p>
<p>This blog is part of an on-going series of posts that go into the details of building an Electron app in Ember JS. It will detail building for MacOS, Linux, and Windows, then I'll finish off with some closing thoughts and extra tips. </p>
<h2 id="initial-setup">Initial setup</h2>
<p>I’ve created a dummy app which you can <a href="https://github.com/snipline/shopper">download from Github</a> and follow along with. Of course, if you already have an Ember app ready to use, checkout a new branch and give it a try!</p>
<p>The app I’ve created is called Shopper and is a simple shopping list app. It lets you split groceries into different categories and keep track of the items you’ve added to your basket, as well as reorder and delete them.</p>
<p>It uses Ember Mirage for the backend storage - which is really convenient for development, but the data does not persist. If you wish to use this application for real then you will need to add your own backend API.</p>
<p>You’ll also need Yarn, Node (I’m using 10.15), and Ember CLI installed. After running <code>yarn</code>, you should be able view the web version with <code>ember serve</code>.</p>
<p><img src="https://f002.backblazeb2.com/file/ms-uploads/2019-07-08+14.34.27.gif" alt="" /></p>
<p>You will need MacOS to build the MacOS app and Windows to build the Windows app. You can build .deb (For Debian based operating systems) on MacOS with the correct tools installed, but it’s probably easier if you have access to a Linux machine. If you wish to build a Snapcraft package you will need Ubuntu 16.04.</p>
<h2 id="installing-electron">Installing Electron</h2>
<p>Run the following command to add Ember Electron to the app.</p>
<pre data-lang="bash" style="background-color:#2b303b;color:#c0c5ce;" class="language-bash "><code class="language-bash" data-lang="bash"><span style="color:#bf616a;">ember</span><span> install ember-electron
</span></code></pre>
<p>This will install the addon and do the initial set up. This includes creating a new directory, <code>ember-electron</code> which is where we can place Electron related code, configuration and resources.</p>
<ul>
<li><code>main.js</code> - this file is the starting area for changing Electron app behaviour. For example, if you want to set the default window size, you can do it here.</li>
<li><code>electron-forge-config.js</code> - Under the hood, Ember Electron uses Electron Forge to build the app. This file is where we’ll put configuration related to building the app. This includes code signing for for MacOS/Windows.</li>
<li><code>resources/</code> - This is where you can place build related resources. We’ll place the Linux Desktop <code>.ejs</code> file in here as well as the app icon files.</li>
</ul>
<p>Without doing any modifications, let’s try running the Electron app in a development environment. Run the following command from the project root.</p>
<pre data-lang="bash" style="background-color:#2b303b;color:#c0c5ce;" class="language-bash "><code class="language-bash" data-lang="bash"><span style="color:#bf616a;">ember</span><span> electron
</span></code></pre>
<p>You should be greeted with an app like the screenshot below (Or relatively similar if you’re on Windows/Linux).</p>
<p><img src="https://f002.backblazeb2.com/file/ms-uploads/Screenshot+2019-07-08+at+14.46.28.png" alt="" /></p>
<h2 id="configuring-the-app">Configuring the app</h2>
<p>So before we go onto building the app for release, there are a few tweaks that we should make and a few to take into consideration.</p>
<ul>
<li>How to change the default window size</li>
<li>(MacOS) Closing the app from the window and clicking the Dock icon doesn’t reopen the app.</li>
<li>How to set a minimum width/height for the app.</li>
<li>(MacOS) How to change the title bar style.</li>
<li>How to add items to the menu bar.</li>
</ul>
<p>To configure all of these we need to update the <code>ember-electron/main.js</code> file.</p>
<p>First of all, lets move the mainWindow stuff into it’s own function and call this function from the <code>ready</code> event.</p>
<pre data-lang="js" style="background-color:#2b303b;color:#c0c5ce;" class="language-js "><code class="language-js" data-lang="js"><span style="color:#bf616a;">app</span><span>.</span><span style="color:#8fa1b3;">on</span><span>('</span><span style="color:#a3be8c;">ready</span><span>', () </span><span style="color:#b48ead;">=> </span><span>{
</span><span> </span><span style="color:#8fa1b3;">loadApp</span><span>();
</span><span>});
</span><span>
</span><span style="color:#65737e;">// Create a new variable for the main window
</span><span style="color:#b48ead;">var </span><span style="color:#bf616a;">mainWindow </span><span>= </span><span style="color:#d08770;">null</span><span>;
</span><span>
</span><span style="color:#b48ead;">function </span><span style="color:#8fa1b3;">loadApp</span><span>() {
</span><span>
</span><span> </span><span style="color:#bf616a;">mainWindow </span><span>= new BrowserWindow({
</span><span> width: </span><span style="color:#d08770;">800</span><span>,
</span><span> height: </span><span style="color:#d08770;">600</span><span>,
</span><span> });
</span><span>
</span><span> </span><span style="color:#65737e;">// If you want to open up dev tools programmatically, call
</span><span> </span><span style="color:#65737e;">// mainWindow.openDevTools();
</span><span>
</span><span> </span><span style="color:#b48ead;">const </span><span style="color:#bf616a;">emberAppLocation </span><span>= '</span><span style="color:#a3be8c;">serve://dist</span><span>';
</span><span>
</span><span> </span><span style="color:#65737e;">// Load the ember application using our custom protocol/scheme
</span><span> </span><span style="color:#bf616a;">mainWindow</span><span>.</span><span style="color:#8fa1b3;">loadURL</span><span>(</span><span style="color:#bf616a;">emberAppLocation</span><span>);
</span><span>
</span><span> </span><span style="color:#65737e;">// If a loading operation goes wrong, we'll send Electron back to
</span><span> </span><span style="color:#65737e;">// Ember App entry point
</span><span> </span><span style="color:#bf616a;">mainWindow</span><span>.</span><span style="color:#bf616a;">webContents</span><span>.</span><span style="color:#8fa1b3;">on</span><span>('</span><span style="color:#a3be8c;">did-fail-load</span><span>', () </span><span style="color:#b48ead;">=> </span><span>{
</span><span> </span><span style="color:#bf616a;">mainWindow</span><span>.</span><span style="color:#8fa1b3;">loadURL</span><span>(</span><span style="color:#bf616a;">emberAppLocation</span><span>);
</span><span> });
</span><span>
</span><span> </span><span style="color:#bf616a;">mainWindow</span><span>.</span><span style="color:#bf616a;">webContents</span><span>.</span><span style="color:#8fa1b3;">on</span><span>('</span><span style="color:#a3be8c;">crashed</span><span>', () </span><span style="color:#b48ead;">=> </span><span>{
</span><span> </span><span style="color:#ebcb8b;">console</span><span>.</span><span style="color:#96b5b4;">log</span><span>('</span><span style="color:#a3be8c;">Your Ember app (or other code) in the main window has crashed.</span><span>');
</span><span> </span><span style="color:#ebcb8b;">console</span><span>.</span><span style="color:#96b5b4;">log</span><span>('</span><span style="color:#a3be8c;">This is a serious issue that needs to be handled and/or debugged.</span><span>');
</span><span> });
</span><span>
</span><span> </span><span style="color:#bf616a;">mainWindow</span><span>.</span><span style="color:#8fa1b3;">on</span><span>('</span><span style="color:#a3be8c;">unresponsive</span><span>', () </span><span style="color:#b48ead;">=> </span><span>{
</span><span> </span><span style="color:#ebcb8b;">console</span><span>.</span><span style="color:#96b5b4;">log</span><span>('</span><span style="color:#a3be8c;">Your Ember app (or other code) has made the window unresponsive.</span><span>');
</span><span> });
</span><span>
</span><span> </span><span style="color:#bf616a;">mainWindow</span><span>.</span><span style="color:#8fa1b3;">on</span><span>('</span><span style="color:#a3be8c;">responsive</span><span>', () </span><span style="color:#b48ead;">=> </span><span>{
</span><span> </span><span style="color:#ebcb8b;">console</span><span>.</span><span style="color:#96b5b4;">log</span><span>('</span><span style="color:#a3be8c;">The main window has become responsive again.</span><span>');
</span><span> });
</span><span>}
</span></code></pre>
<p>To change the default window size and the minimum window size look for the <code>loadApp</code> function. You can see the default <code>width</code> and <code>height</code> is already set. To set the minimum add the following parameters. We’ll also set it to centre the app by default here as well.</p>
<pre data-lang="bash" style="background-color:#2b303b;color:#c0c5ce;" class="language-bash "><code class="language-bash" data-lang="bash"><span> </span><span style="color:#bf616a;">minWidth:</span><span> 400,
</span><span> </span><span style="color:#bf616a;">minHeight:</span><span> 400,
</span><span> </span><span style="color:#bf616a;">center:</span><span> true,
</span></code></pre>
<p>If you’re on MacOS you can use the transparent title bar style which many apps prefer. If you do this, you will need to update your CSS to make the window draggable.</p>
<p>In the same <code>loadApp</code> method, add the following</p>
<pre data-lang="bash" style="background-color:#2b303b;color:#c0c5ce;" class="language-bash "><code class="language-bash" data-lang="bash"><span> </span><span style="color:#bf616a;">titleBarStyle: </span><span>'</span><span style="color:#a3be8c;">hidden</span><span>',
</span></code></pre>
<p>Then in your app css (For the Shopper app this is <code>app/styles/app.css</code> add the following:</p>
<pre data-lang="bash" style="background-color:#2b303b;color:#c0c5ce;" class="language-bash "><code class="language-bash" data-lang="bash"><span style="color:#bf616a;">html,</span><span> body {
</span><span> -webkit-app-region: drag;
</span><span>}
</span><span style="color:#bf616a;">input,</span><span> select, textarea, button, a {
</span><span> -webkit-app-region: no-drag;
</span><span>}
</span></code></pre>
<p>In MacOS, if you try pressing the red close icon in the app window and reopen from the Dock nothing will happen. To fix this we need add an event hook. Place this above the <code>loadApp</code> function</p>
<pre data-lang="js" style="background-color:#2b303b;color:#c0c5ce;" class="language-js "><code class="language-js" data-lang="js"><span style="color:#bf616a;">app</span><span>.</span><span style="color:#8fa1b3;">on</span><span>('</span><span style="color:#a3be8c;">activate</span><span>', </span><span style="color:#b48ead;">function </span><span>() {
</span><span> </span><span style="color:#b48ead;">if </span><span>(</span><span style="color:#bf616a;">mainWindow </span><span>=== </span><span style="color:#d08770;">null</span><span>) {
</span><span> </span><span style="color:#8fa1b3;">loadApp</span><span>();
</span><span> }
</span><span>});
</span></code></pre>
<p>Add this code below the <code>mainWindow</code> definition in the <code>loadApp </code> function</p>
<pre data-lang="js" style="background-color:#2b303b;color:#c0c5ce;" class="language-js "><code class="language-js" data-lang="js"><span style="color:#bf616a;">mainWindow</span><span>.</span><span style="color:#8fa1b3;">on</span><span>('</span><span style="color:#a3be8c;">closed</span><span>', () </span><span style="color:#b48ead;">=> </span><span>{
</span><span> </span><span style="color:#bf616a;">mainWindow </span><span>= </span><span style="color:#d08770;">null</span><span>;
</span><span>})
</span></code></pre>
<p>We can keep the Dock icon loaded when all windows are closed by preventing it from quitting in the <code>window-all-closed</code> event.</p>
<pre data-lang="js" style="background-color:#2b303b;color:#c0c5ce;" class="language-js "><code class="language-js" data-lang="js"><span style="color:#bf616a;">app</span><span>.</span><span style="color:#8fa1b3;">on</span><span>('</span><span style="color:#a3be8c;">window-all-closed</span><span>', () </span><span style="color:#b48ead;">=> </span><span>{
</span><span> </span><span style="color:#b48ead;">if </span><span>(process.platform !== '</span><span style="color:#a3be8c;">darwin</span><span>') {
</span><span> </span><span style="color:#bf616a;">app</span><span>.</span><span style="color:#8fa1b3;">quit</span><span>();
</span><span> }
</span><span>});
</span></code></pre>
<p>For the sake of example, if you wish to modify the menu items (File, Help, etc) we can do this here as well. Note that I tend to only do this for production releases as it removes the Developer Inspector and other useful items. Put this inside the <code>loadApp</code> function below everything else and add a new variable called template near line 6.</p>
<pre data-lang="js" style="background-color:#2b303b;color:#c0c5ce;" class="language-js "><code class="language-js" data-lang="js"><span style="color:#65737e;">// Add Menu to require('electron');
</span><span style="color:#b48ead;">const </span><span>{ </span><span style="color:#bf616a;">app</span><span>, </span><span style="color:#bf616a;">BrowserWindow</span><span>, </span><span style="color:#bf616a;">protocol</span><span>, </span><span style="color:#bf616a;">Menu </span><span>} = </span><span style="color:#96b5b4;">require</span><span>('</span><span style="color:#a3be8c;">electron</span><span>');
</span><span style="color:#65737e;">// Add a new variable for the menu template.
</span><span style="color:#b48ead;">var </span><span style="color:#bf616a;">mainWindow</span><span>, </span><span style="color:#bf616a;">template </span><span>= </span><span style="color:#d08770;">null</span><span>;
</span><span style="color:#65737e;">// ...
</span><span style="color:#b48ead;">function </span><span style="color:#8fa1b3;">loadApp</span><span>() {
</span><span> </span><span style="color:#65737e;">//mainWindow = ...
</span><span> </span><span style="color:#b48ead;">if</span><span>(process.env.</span><span style="color:#bf616a;">ELECTRON_ENV </span><span>!== "</span><span style="color:#a3be8c;">development</span><span>") {
</span><span> </span><span style="color:#bf616a;">template </span><span>= [
</span><span> {
</span><span> label: "</span><span style="color:#a3be8c;">Edit</span><span>",
</span><span> submenu: [
</span><span> { label: "</span><span style="color:#a3be8c;">Undo</span><span>", accelerator: "</span><span style="color:#a3be8c;">CmdOrCtrl+Z</span><span>", selector: "</span><span style="color:#a3be8c;">undo:</span><span>" },
</span><span> { label: "</span><span style="color:#a3be8c;">Redo</span><span>", accelerator: "</span><span style="color:#a3be8c;">Shift+CmdOrCtrl+Z</span><span>", selector: "</span><span style="color:#a3be8c;">redo:</span><span>" },
</span><span> { type: "</span><span style="color:#a3be8c;">separator</span><span>" },
</span><span> { label: "</span><span style="color:#a3be8c;">Cut</span><span>", accelerator: "</span><span style="color:#a3be8c;">CmdOrCtrl+X</span><span>", selector: "</span><span style="color:#a3be8c;">cut:</span><span>" },
</span><span> { label: "</span><span style="color:#a3be8c;">Copy</span><span>", accelerator: "</span><span style="color:#a3be8c;">CmdOrCtrl+C</span><span>", selector: "</span><span style="color:#a3be8c;">copy:</span><span>" },
</span><span> { label: "</span><span style="color:#a3be8c;">Paste</span><span>", accelerator: "</span><span style="color:#a3be8c;">CmdOrCtrl+V</span><span>", selector: "</span><span style="color:#a3be8c;">paste:</span><span>" },
</span><span> { label: "</span><span style="color:#a3be8c;">Select All</span><span>", accelerator: "</span><span style="color:#a3be8c;">CmdOrCtrl+A</span><span>", selector: "</span><span style="color:#a3be8c;">selectAll:</span><span>" }
</span><span> ]
</span><span> },{
</span><span> label: '</span><span style="color:#a3be8c;">Help</span><span>',
</span><span> submenu: [
</span><span> {
</span><span> label: '</span><span style="color:#a3be8c;">Learn More</span><span>',
</span><span> </span><span style="color:#8fa1b3;">click </span><span>() { </span><span style="color:#96b5b4;">require</span><span>('</span><span style="color:#a3be8c;">electron</span><span>').</span><span style="color:#bf616a;">shell</span><span>.</span><span style="color:#8fa1b3;">openExternal</span><span>('</span><span style="color:#a3be8c;">https://dev.to/mitchartemis</span><span>') }
</span><span> }
</span><span> ]
</span><span> }];
</span><span> </span><span style="color:#b48ead;">if </span><span>(process.platform === '</span><span style="color:#a3be8c;">darwin</span><span>') {
</span><span> </span><span style="color:#bf616a;">template</span><span>.</span><span style="color:#96b5b4;">unshift</span><span>({
</span><span> label: </span><span style="color:#bf616a;">app</span><span>.</span><span style="color:#8fa1b3;">getName</span><span>(),
</span><span> submenu: [
</span><span> {label: '</span><span style="color:#a3be8c;">Check for updates</span><span>', </span><span style="color:#8fa1b3;">click</span><span>() { </span><span style="color:#96b5b4;">require</span><span>('</span><span style="color:#a3be8c;">electron</span><span>').</span><span style="color:#bf616a;">shell</span><span>.</span><span style="color:#8fa1b3;">openExternal</span><span>(`</span><span style="color:#a3be8c;">https://dev.to/mitchartemis</span><span>`); }},
</span><span> {role: '</span><span style="color:#a3be8c;">about</span><span>'},
</span><span> {type: '</span><span style="color:#a3be8c;">separator</span><span>'},
</span><span> {role: '</span><span style="color:#a3be8c;">quit</span><span>'}
</span><span> ]
</span><span> })
</span><span> } </span><span style="color:#b48ead;">else </span><span>{
</span><span> </span><span style="color:#bf616a;">template</span><span>.</span><span style="color:#96b5b4;">unshift</span><span>({
</span><span> label: "</span><span style="color:#a3be8c;">File</span><span>",
</span><span> submenu: [
</span><span> {label: '</span><span style="color:#a3be8c;">Check for updates</span><span>', </span><span style="color:#8fa1b3;">click</span><span>() { </span><span style="color:#96b5b4;">require</span><span>('</span><span style="color:#a3be8c;">electron</span><span>').</span><span style="color:#bf616a;">shell</span><span>.</span><span style="color:#8fa1b3;">openExternal</span><span>(`</span><span style="color:#a3be8c;">https://dev.to/mitchartemis</span><span>`); }},
</span><span> {type: '</span><span style="color:#a3be8c;">separator</span><span>'},
</span><span> {role: '</span><span style="color:#a3be8c;">quit</span><span>'}
</span><span> ]
</span><span> })
</span><span> }
</span><span> </span><span style="color:#b48ead;">const </span><span style="color:#bf616a;">menu </span><span>= </span><span style="color:#bf616a;">Menu</span><span>.</span><span style="color:#8fa1b3;">buildFromTemplate</span><span>(</span><span style="color:#bf616a;">template</span><span>)
</span><span> </span><span style="color:#bf616a;">Menu</span><span>.</span><span style="color:#8fa1b3;">setApplicationMenu</span><span>(</span><span style="color:#bf616a;">menu</span><span>)
</span><span> }
</span><span>}
</span></code></pre>
<p>There are a few things going on here, first we check if we’re in development mode, if we’re not then we create a Menu from our own template.</p>
<p>The <code>label</code> attribute allows us to specify the top level names and inside the <code>submenu</code> we place all of the menu options.</p>
<p>We can create links to external websites like so:</p>
<pre data-lang="js" style="background-color:#2b303b;color:#c0c5ce;" class="language-js "><code class="language-js" data-lang="js"><span>{label: '</span><span style="color:#a3be8c;">Check for updates</span><span>', </span><span style="color:#8fa1b3;">click</span><span>() { </span><span style="color:#96b5b4;">require</span><span>('</span><span style="color:#a3be8c;">electron</span><span>').</span><span style="color:#bf616a;">shell</span><span>.</span><span style="color:#8fa1b3;">openExternal</span><span>(`</span><span style="color:#a3be8c;">https://dev.to/mitchartemis</span><span>`); }}
</span></code></pre>
<p>Create separators</p>
<pre data-lang="js" style="background-color:#2b303b;color:#c0c5ce;" class="language-js "><code class="language-js" data-lang="js"><span>{type: '</span><span style="color:#a3be8c;">separator</span><span>'},
</span></code></pre>
<p>Use predefined functionality with <code>roles</code></p>
<pre data-lang="js" style="background-color:#2b303b;color:#c0c5ce;" class="language-js "><code class="language-js" data-lang="js"><span>{role: '</span><span style="color:#a3be8c;">about</span><span>'}
</span><span>{role: '</span><span style="color:#a3be8c;">quit</span><span>'}
</span></code></pre>
<p>And specify shortcuts for pre-existing methods as well.</p>
<pre data-lang="js" style="background-color:#2b303b;color:#c0c5ce;" class="language-js "><code class="language-js" data-lang="js"><span>{ label: "</span><span style="color:#a3be8c;">Select All</span><span>", accelerator: "</span><span style="color:#a3be8c;">CmdOrCtrl+A</span><span>", selector: "</span><span style="color:#a3be8c;">selectAll:</span><span>" }
</span></code></pre>
<p>Now it’s time to <code>Ctrl+C</code> the current running app and re-run it to see the results.</p>
<p><img src="https://f002.backblazeb2.com/file/ms-uploads/Screenshot+2019-07-08+at+14.42.35.png" alt="" /></p>
<p>In MacOS you should be able to click-and-drag the whole app window (except form elements) and close and reopen the app from the dock. For all platforms there should now be a minimum of 400x400 window size.</p>
<h2 id="prepping-for-a-release">Prepping for a release</h2>
<p>If you’re using your own app, change the <code>electron-prebuild-compile</code> to use v3 rather than v4 in your <code>package.json</code> dev dependencies and run <code>yarn upgrade</code>.</p>
<p><code>"electron-prebuilt-compile": "3.0.13", </code>
If you don’t do this you will be unable to use the <code>electron make</code> command.</p>
<h2 id="the-app-icon">The app icon</h2>
<p>During development the Electron app uses the default Electron development icon, but when you build a production release you’re able to use your own icon.</p>
<p>The icon will need to be available for each platform.</p>
<ul>
<li><code>.icns</code> for MacOS</li>
<li>256x256 <code>.ico</code> for Windows</li>
<li>1024x1024 <code>.png</code> for Linux</li>
</ul>
<p>The MacOS <code>.icns</code> file can be made a few ways, but at the very least you’ll need a 1024x1024 transparent png to convert from.</p>
<p>I highly recommend the free Mac app, <a href="https://apps.apple.com/us/app/image2icon-make-your-icons/id992115977">Image2icon</a> (Also available in Setapp). Plug in your image and export to <code>.icns</code>. As a bonus, you can also use this to create your Windows <code>.ico</code> file, too — although this comes at a cost. There are plenty of free online <code>.png</code> to <code>.ico</code> converters out there.</p>
<p>If you’d rather make the <code>.icns</code> file manually, there’s an <a href="https://stackoverflow.com/a/20703594">excellent post on StackOverflow</a> on how to do that.</p>
<p>Once you have all the images place them in the <code>ember-electron/resources</code> directory. It’s really important to give them the same name. I’ve included the icon files for Shopper in the Github repository.</p>
<h2 id="version-number">Version number</h2>
<p>Make sure before building to update your version number! You can do this from the <code>~/package.json</code>. This will show in outputted build file and the MacOS About menu.</p>
<h2 id="what-we-ve-done-so-far">What we’ve done so far</h2>
<p>That’s all for part one. We’ve covered a lot of ground in a small amount of time, including integrating Electron into an Ember App, configuring the app for it’s first release, and going over some extra details such as creating icons and menu items.</p>
<p>In part two we’ll create the first MacOS release with code signing.</p>
<p><a href="/articles/making-an-electron-app-with-ember-js-part-2-macos">Click here to read part two.</a></p>
Making an Electron App with Ember JS Part #2: MacOS2019-07-09T00:00:00+00:002019-07-09T00:00:00+00:00https://www.fullstackstanley.com/articles/making-an-electron-app-with-ember-js-part-2-macos/<p>This is part two in the series of blog posts “Making an Electron App with Ember JS” where I go over how I built my app <a href="https://snipline.io">Snipline</a> for the web, Mac, Windows, and Linux.</p>
<span id="continue-reading"></span>
<p>This post assumes you’ve read part one, if you haven’t then I highly recommend it. Now, on with the show!</p>
<h2 id="building-for-macos">Building for MacOS</h2>
<p>Before building the app we need prepare it for code signing. For this you will need to have an Apple Developer Account which if you haven’t already, you can get from the <a href="http://developer.apple.com">Apple Developer website</a>. Note that this costs a yearly fee of $99.</p>
<p>Why is code signing important? I’m glad you asked! Code signing makes sure the files that your users download hasn’t been tampered with and comes from the developer that you expect. Without it, MacOS and Windows will go warn users about running the app and to a certain extent prevent them from doing so.</p>
<p>You should be able to follow along without code signing for educational purposes but for a production app I would highly recommend it.</p>
<p>Once you have the Developer account set up create a “Developer ID Application” certificate, download and install it on your Mac machine.</p>
<p>In <code>ember-electron/electron-forge-config.js</code> add the following:</p>
<pre data-lang="js" style="background-color:#2b303b;color:#c0c5ce;" class="language-js "><code class="language-js" data-lang="js"><span style="color:#65737e;">// At the top
</span><span style="color:#b48ead;">const </span><span style="color:#bf616a;">path </span><span>= </span><span style="color:#96b5b4;">require</span><span>('</span><span style="color:#a3be8c;">path</span><span>');
</span><span style="color:#b48ead;">const </span><span style="color:#bf616a;">rootPath </span><span>= </span><span style="color:#bf616a;">path</span><span>.</span><span style="color:#96b5b4;">join</span><span>('</span><span style="color:#a3be8c;">./</span><span>');
</span><span>
</span><span style="color:#b48ead;">function </span><span style="color:#8fa1b3;">getCodesignIdentity</span><span>() {
</span><span> </span><span style="color:#b48ead;">if </span><span>(process.platform !== '</span><span style="color:#a3be8c;">darwin</span><span>') {
</span><span> </span><span style="color:#b48ead;">return</span><span>;
</span><span> }
</span><span>
</span><span> </span><span style="color:#b48ead;">if </span><span>(process.env.</span><span style="color:#bf616a;">CODESIGN_IDENTITY</span><span>) {
</span><span> </span><span style="color:#b48ead;">return </span><span>process.env.</span><span style="color:#bf616a;">CODESIGN_IDENTITY</span><span>;
</span><span> } </span><span style="color:#b48ead;">else </span><span>{
</span><span> </span><span style="color:#ebcb8b;">console</span><span>.</span><span style="color:#96b5b4;">log</span><span>('</span><span style="color:#a3be8c;">Codesigning identity can not be found, release build will fail</span><span>');
</span><span> </span><span style="color:#ebcb8b;">console</span><span>.</span><span style="color:#96b5b4;">log</span><span>('</span><span style="color:#a3be8c;">To fix, set CODESIGN_IDENTITY</span><span>');
</span><span> }
</span><span>}
</span><span>
</span><span style="color:#b48ead;">function </span><span style="color:#8fa1b3;">getBundleId</span><span>() {
</span><span> </span><span style="color:#b48ead;">if </span><span>(process.platform !== '</span><span style="color:#a3be8c;">darwin</span><span>') {
</span><span> </span><span style="color:#b48ead;">return</span><span>;
</span><span> }
</span><span>
</span><span> </span><span style="color:#b48ead;">if </span><span>(process.env.</span><span style="color:#bf616a;">BUNDLE_ID</span><span>) {
</span><span> </span><span style="color:#b48ead;">return </span><span>process.env.</span><span style="color:#bf616a;">BUNDLE_ID</span><span>;
</span><span> } </span><span style="color:#b48ead;">else </span><span>{
</span><span> </span><span style="color:#ebcb8b;">console</span><span>.</span><span style="color:#96b5b4;">log</span><span>('</span><span style="color:#a3be8c;">bundle id can not be found, release build will fail</span><span>');
</span><span> </span><span style="color:#ebcb8b;">console</span><span>.</span><span style="color:#96b5b4;">log</span><span>('</span><span style="color:#a3be8c;">To fix, set BUNDLE_ID</span><span>');
</span><span> }
</span><span>}
</span><span>
</span><span style="color:#65737e;">// Replace electronPackagerConfig with this
</span><span>"</span><span style="color:#a3be8c;">electronPackagerConfig</span><span>": {
</span><span> "</span><span style="color:#a3be8c;">packageManager</span><span>": "</span><span style="color:#a3be8c;">yarn</span><span>",
</span><span> name: "</span><span style="color:#a3be8c;">Shopper</span><span>",
</span><span> icon: </span><span style="color:#bf616a;">path</span><span>.</span><span style="color:#96b5b4;">join</span><span>(</span><span style="color:#bf616a;">rootPath</span><span>, '</span><span style="color:#a3be8c;">ember-electron</span><span>', '</span><span style="color:#a3be8c;">resources</span><span>', '</span><span style="color:#a3be8c;">shopper</span><span>'),
</span><span> versionString: {
</span><span> CompanyName: '</span><span style="color:#a3be8c;">Acme Ltd</span><span>',
</span><span> FileDescription: '</span><span style="color:#a3be8c;">Shpoper for Desktop</span><span>',
</span><span> ProductName: '</span><span style="color:#a3be8c;">Shopper</span><span>',
</span><span> InternalName: '</span><span style="color:#a3be8c;">Shopper</span><span>'
</span><span> },
</span><span> "</span><span style="color:#a3be8c;">osxSign</span><span>": {
</span><span> "</span><span style="color:#a3be8c;">identity</span><span>": </span><span style="color:#8fa1b3;">getCodesignIdentity</span><span>()
</span><span> },
</span><span> "</span><span style="color:#a3be8c;">appBundleId</span><span>": </span><span style="color:#8fa1b3;">getBundleId</span><span>(),
</span><span> "</span><span style="color:#a3be8c;">appCategoryType</span><span>": "</span><span style="color:#a3be8c;">app-category-type=public.app-category.developer-tools</span><span>",
</span><span> },
</span><span>
</span><span style="color:#65737e;">// At the bottom of module.exports
</span><span>electronInstallerDMG: {
</span><span> title: '</span><span style="color:#a3be8c;">Shopper</span><span>',
</span><span> icon: </span><span style="color:#bf616a;">path</span><span>.</span><span style="color:#96b5b4;">join</span><span>(</span><span style="color:#bf616a;">rootPath</span><span>, '</span><span style="color:#a3be8c;">ember-electron</span><span>', '</span><span style="color:#a3be8c;">resources</span><span>', '</span><span style="color:#a3be8c;">shopper.icns</span><span>'),
</span><span>
</span><span> iconsize: </span><span style="color:#d08770;">100</span><span>,
</span><span> window: {
</span><span> size: {
</span><span> width: </span><span style="color:#d08770;">600</span><span>,
</span><span> height: </span><span style="color:#d08770;">571
</span><span> }
</span><span> }
</span><span>},
</span></code></pre>
<p>There’s one extra step before we can run. Code signing <a href="https://www.fullstackstanley.com/articles/making-an-electron-app-with-ember-js-part-2-5-macos-notarisation/">On a Mac</a> no longer allows any file in an app bundle to have an extended attribute containing a resource fork or Finder info. This will most likely apply to any assets that you’ve created and you can debug it by running</p>
<pre data-lang="bash" style="background-color:#2b303b;color:#c0c5ce;" class="language-bash "><code class="language-bash" data-lang="bash"><span style="color:#bf616a;">xattr -lr</span><span> .
</span></code></pre>
<p>In the Shopper app it only affects the newly created icons. We can fix this by running the following command. In your own apps you will need to use both commands to find and fix any assets. Without doing this your app will build, but code signing might fail.</p>
<pre data-lang="bash" style="background-color:#2b303b;color:#c0c5ce;" class="language-bash "><code class="language-bash" data-lang="bash"><span style="color:#bf616a;">xattr -cr</span><span> ember-electron/resources
</span></code></pre>
<p>Now for the fun part. Take the following build command and update the <code>CODESIGN_IDENTITY</code> and <code>BUNDLE_ID</code> variables. The bundle ID should be a unique code, most people use their domain name in reverse with unique subdomain. </p>
<p>Run the command, go grab yourself a hot cup of tea, and when you’re back you should have a <code>.zip</code> in <code>electron-out/make/</code> file containing the app.</p>
<p><code>env CODESIGN_IDENTITY="Developer ID Application: <Name> (<ID>)" env BUNDLE_ID="io.exampledomain.desktop" ELECTRON_ENV=production ember electron:make --environment=production </code></p>
<p>Unzip it and run it, you should see the new app, dock icon and all!</p>
<h2 id="creating-the-dmg-installer">Creating the DMG installer</h2>
<p>An optional nice touch is to create a DMG file which will guide the user into moving your app into their <code>/Applications</code> directory.</p>
<p>For this I use an open source tool called <a href="https://github.com/sindresorhus/create-dmg">create-dmg</a>. It’s fairly simple to use and will pick up your code signing cert automatically.</p>
<pre data-lang="bash" style="background-color:#2b303b;color:#c0c5ce;" class="language-bash "><code class="language-bash" data-lang="bash"><span style="color:#96b5b4;">cd</span><span> electron-out/make/
</span><span style="color:#bf616a;">rm -rf</span><span> Shopper.app
</span><span style="color:#bf616a;">unzip</span><span> Shopper-darwin-x64-0.1.0.zip
</span><span style="color:#bf616a;">create-dmg</span><span> Shopper.app ./
</span></code></pre>
<h2 id="what-we-ve-done-so-far">What we’ve done so far</h2>
<p>We’ve configured the Electron app to generate a code signed MacOS application and bundled it into an easy-to-install DMG file.</p>
<p>In the next chapter we’ll take a look at building for Linux!</p>
Configuring Ember JS Analytics for GDPR2018-07-18T00:00:00+00:002018-07-18T00:00:00+00:00https://www.fullstackstanley.com/articles/configuring-ember-js-analytics-for-gdpr/<p>One of the issues I ran into while developing a new Ember JS app was letting the user configure tracking libraries such as Google Analytics for GDPR compliance.</p>
<span id="continue-reading"></span>
<p>Fortunately it’s not that difficult! I’ve used two packages for this: <a href="https://github.com/poteto/ember-metrics">ember-metrics</a> and <a href="https://github.com/funkensturm/ember-local-storage">ember-local-storage</a>. If you’re conservative with your external dependencies then you could get away without using ember-local-storage and just use native Javascript for this. I chose to use it for convenience.</p>
<p>Make sure you install the two above mentioned packages</p>
<pre data-lang="bash" style="background-color:#2b303b;color:#c0c5ce;" class="language-bash "><code class="language-bash" data-lang="bash"><span style="color:#bf616a;">ember</span><span> install ember-local-storage
</span><span style="color:#bf616a;">ember</span><span> install ember-metrics
</span></code></pre>
<p>Add your configuration for ember-metrics in the <code>config/environment.js</code> <code>ENV</code> hash, here’s mine as an example:</p>
<pre data-lang="js" style="background-color:#2b303b;color:#c0c5ce;" class="language-js "><code class="language-js" data-lang="js"><span> </span><span style="color:#65737e;">//...
</span><span> metricsAdapters: [
</span><span> {
</span><span> name: '</span><span style="color:#a3be8c;">GoogleAnalytics</span><span>',
</span><span> environments: ['</span><span style="color:#a3be8c;">development</span><span>', '</span><span style="color:#a3be8c;">production</span><span>'],
</span><span> config: {
</span><span> id: '</span><span style="color:#a3be8c;">UA-XXXXXXXX-1</span><span>',
</span><span> anonymizeIp: </span><span style="color:#d08770;">true</span><span>,
</span><span> </span><span style="color:#65737e;">// Use analytics_debug.js in development
</span><span> debug: </span><span style="color:#bf616a;">environment </span><span>=== '</span><span style="color:#a3be8c;">development</span><span>',
</span><span> </span><span style="color:#65737e;">// Use verbose tracing of GA events
</span><span> trace: </span><span style="color:#bf616a;">environment </span><span>=== '</span><span style="color:#a3be8c;">development</span><span>',
</span><span> </span><span style="color:#65737e;">// Ensure development env hits aren't sent to GA
</span><span> sendHitTask: </span><span style="color:#bf616a;">environment </span><span>!== '</span><span style="color:#a3be8c;">development</span><span>',
</span><span> </span><span style="color:#65737e;">// Specify Google Analytics plugins
</span><span> require: []
</span><span> }
</span><span> }
</span><span> ],
</span><span> </span><span style="color:#65737e;">//...
</span></code></pre>
<p>Notice that we’re using <code>anonymizeIP</code>. This is a feature of Google Analytics which, you guessed it, anonymises IP addresses. This is optional and will depend on your project’s requirements.</p>
<p>Don’t forget to add the <code>contentSecurityPolicy</code> settings if you’re using <code>ember-content-security-policy</code> (More about that in the Ember Metrics README).</p>
<pre data-lang="js" style="background-color:#2b303b;color:#c0c5ce;" class="language-js "><code class="language-js" data-lang="js"><span> </span><span style="color:#65737e;">//...
</span><span> contentSecurityPolicy: {
</span><span> '</span><span style="color:#a3be8c;">default-src</span><span>': "</span><span style="color:#a3be8c;">'none'</span><span>",
</span><span> '</span><span style="color:#a3be8c;">script-src</span><span>': "</span><span style="color:#a3be8c;">'self' www.google-analytics.com</span><span>",
</span><span> '</span><span style="color:#a3be8c;">font-src</span><span>': "</span><span style="color:#a3be8c;">'self'</span><span>",
</span><span> '</span><span style="color:#a3be8c;">connect-src</span><span>': "</span><span style="color:#a3be8c;">'self' www.google-analytics.com</span><span>",
</span><span> '</span><span style="color:#a3be8c;">img-src</span><span>': "</span><span style="color:#a3be8c;">'self'</span><span>",
</span><span> '</span><span style="color:#a3be8c;">style-src</span><span>': "</span><span style="color:#a3be8c;">'self'</span><span>",
</span><span> '</span><span style="color:#a3be8c;">media-src</span><span>': "</span><span style="color:#a3be8c;">'self'</span><span>"
</span><span> }
</span><span> </span><span style="color:#65737e;">//...
</span></code></pre>
<p>Update your <code>app/router.js</code> as instructed on Ember Metrics. We’ll add ember-local-storage to check for the user’s preferences before sending tracking requests as well. I’ve named the storage scope <code>settings</code>, however, you can call it what you prefer. See <code>ember-local-storage</code> README for details.</p>
<p>Within the <code>_trackPage()</code> method we’re checking if <code>settings.analytics</code> is set to <code>true</code>, if it is then we’ll send the request. You could also make other categories, e.g. <code>marketing</code> and send requests for those the same way. </p>
<p>Ember Metrics supports a bunch of the most popular tools out of the box, including Intercom. You can also add your own custom scripts as well.</p>
<pre data-lang="js" style="background-color:#2b303b;color:#c0c5ce;" class="language-js "><code class="language-js" data-lang="js"><span style="color:#b48ead;">import </span><span style="color:#bf616a;">EmberRouter </span><span style="color:#b48ead;">from </span><span>'</span><span style="color:#a3be8c;">@ember/routing/router</span><span>';
</span><span style="color:#b48ead;">import </span><span style="color:#bf616a;">config </span><span style="color:#b48ead;">from </span><span>'</span><span style="color:#a3be8c;">./config/environment</span><span>';
</span><span style="color:#b48ead;">import </span><span>{ </span><span style="color:#bf616a;">inject </span><span style="color:#b48ead;">as </span><span style="color:#bf616a;">service </span><span>} </span><span style="color:#b48ead;">from </span><span>'</span><span style="color:#a3be8c;">@ember/service</span><span>';
</span><span style="color:#b48ead;">import </span><span>{ </span><span style="color:#bf616a;">scheduleOnce </span><span>} </span><span style="color:#b48ead;">from </span><span>'</span><span style="color:#a3be8c;">@ember/runloop</span><span>';
</span><span style="color:#b48ead;">import </span><span>{ </span><span style="color:#bf616a;">get </span><span>} </span><span style="color:#b48ead;">from </span><span>'</span><span style="color:#a3be8c;">@ember/object</span><span>';
</span><span style="color:#b48ead;">import </span><span>{ </span><span style="color:#bf616a;">storageFor </span><span>} </span><span style="color:#b48ead;">from </span><span>'</span><span style="color:#a3be8c;">ember-local-storage</span><span>';
</span><span>
</span><span style="color:#b48ead;">const </span><span style="color:#bf616a;">Router </span><span>= </span><span style="color:#bf616a;">EmberRouter</span><span>.</span><span style="color:#8fa1b3;">extend</span><span>({
</span><span> </span><span style="color:#65737e;">// Import ember local storage
</span><span> settings: </span><span style="color:#8fa1b3;">storageFor</span><span>('</span><span style="color:#a3be8c;">settings</span><span>'),
</span><span> location: </span><span style="color:#bf616a;">config</span><span>.</span><span style="color:#bf616a;">locationType</span><span>,
</span><span> rootURL: </span><span style="color:#bf616a;">config</span><span>.</span><span style="color:#bf616a;">rootURL</span><span>,
</span><span> metrics: </span><span style="color:#8fa1b3;">service</span><span>(),
</span><span> </span><span style="color:#8fa1b3;">didTransition</span><span>() {
</span><span> </span><span style="color:#bf616a;">this</span><span>.</span><span style="color:#8fa1b3;">_super</span><span>(...</span><span style="color:#bf616a;">arguments</span><span>);
</span><span> </span><span style="color:#bf616a;">this</span><span>.</span><span style="color:#8fa1b3;">_trackPage</span><span>();
</span><span> },
</span><span>
</span><span> </span><span style="color:#8fa1b3;">_trackPage</span><span>() {
</span><span> </span><span style="color:#8fa1b3;">scheduleOnce</span><span>('</span><span style="color:#a3be8c;">afterRender</span><span>', </span><span style="color:#bf616a;">this</span><span>, () </span><span style="color:#b48ead;">=> </span><span>{
</span><span> </span><span style="color:#65737e;">// If user wants analytics then send the request
</span><span> </span><span style="color:#b48ead;">if</span><span>(</span><span style="color:#bf616a;">this</span><span>.</span><span style="color:#96b5b4;">get</span><span>('</span><span style="color:#a3be8c;">settings.analytics</span><span>')) {
</span><span> </span><span style="color:#b48ead;">const </span><span style="color:#bf616a;">page </span><span>= </span><span style="color:#bf616a;">this</span><span>.</span><span style="color:#96b5b4;">get</span><span>('</span><span style="color:#a3be8c;">url</span><span>');
</span><span> </span><span style="color:#b48ead;">const </span><span style="color:#bf616a;">title </span><span>= </span><span style="color:#bf616a;">this</span><span>.</span><span style="color:#8fa1b3;">getWithDefault</span><span>('</span><span style="color:#a3be8c;">currentRouteName</span><span>', '</span><span style="color:#a3be8c;">unknown</span><span>');
</span><span> </span><span style="color:#8fa1b3;">get</span><span>(</span><span style="color:#bf616a;">this</span><span>, '</span><span style="color:#a3be8c;">metrics</span><span>').</span><span style="color:#8fa1b3;">trackPage</span><span>({ </span><span style="color:#bf616a;">page</span><span>, </span><span style="color:#bf616a;">title </span><span>});
</span><span> }
</span><span> });
</span><span> }
</span><span>});
</span><span>
</span><span style="color:#bf616a;">Router</span><span>.</span><span style="color:#8fa1b3;">map</span><span>(</span><span style="color:#b48ead;">function</span><span>() {
</span><span> </span><span style="color:#65737e;">// Your routes here
</span><span> </span><span style="color:#bf616a;">this</span><span>.</span><span style="color:#8fa1b3;">route</span><span>('</span><span style="color:#a3be8c;">login</span><span>');
</span><span> </span><span style="color:#bf616a;">this</span><span>.</span><span style="color:#8fa1b3;">route</span><span>('</span><span style="color:#a3be8c;">about</span><span>');
</span><span>});
</span><span>
</span><span style="color:#b48ead;">export default </span><span style="color:#bf616a;">Router</span><span>;
</span><span>
</span></code></pre>
<p>Now we’ve got the logic in place but the user has no way of updating their preference unless we expect them to update localStorage via the console (If only!).</p>
<p>There are a bunch of ways you can do this, all we need to do is set the <code>settings.analytics</code> in localStorage. I’ve chosen to use a checkbox and I’ve put it on my main <code>index.hbs</code> template. We’ll set the variable initially through the <code>routes/index.js</code> and watch for updates in <code>controller/index.js</code></p>
<pre data-lang="html" style="background-color:#2b303b;color:#c0c5ce;" class="language-html "><code class="language-html" data-lang="html"><span>//templates/index.hbs
</span><span><</span><span style="color:#bf616a;">label</span><span>>{{input type="checkbox" checked=wantsAnalytics }} Enable Analytics</</span><span style="color:#bf616a;">label</span><span>>
</span></code></pre>
<p>Note that by default we’re setting analytics to <code>false</code>. The user will have to opt-in manually to send their analytical data.</p>
<pre data-lang="js" style="background-color:#2b303b;color:#c0c5ce;" class="language-js "><code class="language-js" data-lang="js"><span style="color:#65737e;">//routes/index.js
</span><span style="color:#b48ead;">import </span><span style="color:#bf616a;">Route </span><span style="color:#b48ead;">from </span><span>'</span><span style="color:#a3be8c;">@ember/routing/route</span><span>';
</span><span style="color:#b48ead;">import </span><span>{ </span><span style="color:#bf616a;">storageFor </span><span>} </span><span style="color:#b48ead;">from </span><span>'</span><span style="color:#a3be8c;">ember-local-storage</span><span>';
</span><span>
</span><span style="color:#b48ead;">export default </span><span style="color:#bf616a;">Route</span><span>.</span><span style="color:#8fa1b3;">extend</span><span>({
</span><span> settings: </span><span style="color:#8fa1b3;">storageFor</span><span>('</span><span style="color:#a3be8c;">settings</span><span>'),
</span><span> </span><span style="color:#8fa1b3;">setupController</span><span>(</span><span style="color:#bf616a;">controller</span><span>) {
</span><span> </span><span style="color:#bf616a;">this</span><span>.</span><span style="color:#8fa1b3;">_super</span><span>(...</span><span style="color:#bf616a;">arguments</span><span>)
</span><span> </span><span style="color:#65737e;">// Set default to false to be opt-in, or true to be opt out
</span><span> </span><span style="color:#b48ead;">var </span><span style="color:#bf616a;">wantsAnalytics </span><span>= (</span><span style="color:#bf616a;">this</span><span>.</span><span style="color:#96b5b4;">get</span><span>('</span><span style="color:#a3be8c;">settings.analytics</span><span>') !== </span><span style="color:#d08770;">undefined</span><span>) ? </span><span style="color:#bf616a;">this</span><span>.</span><span style="color:#96b5b4;">get</span><span>('</span><span style="color:#a3be8c;">settings.analytics</span><span>') : </span><span style="color:#d08770;">false</span><span>;
</span><span> </span><span style="color:#bf616a;">controller</span><span>.</span><span style="color:#96b5b4;">set</span><span>('</span><span style="color:#a3be8c;">wantsAnalytics</span><span>', </span><span style="color:#bf616a;">wantsAnalytics</span><span>);
</span><span> },
</span><span>});
</span><span>
</span></code></pre>
<p>In the controller we observe the input checkbox value, once it changes we update the local storage to reflect the new value.</p>
<pre data-lang="js" style="background-color:#2b303b;color:#c0c5ce;" class="language-js "><code class="language-js" data-lang="js"><span style="color:#65737e;">//controllers/index.js
</span><span style="color:#b48ead;">import </span><span style="color:#bf616a;">Controller </span><span style="color:#b48ead;">from </span><span>'</span><span style="color:#a3be8c;">@ember/controller</span><span>';
</span><span style="color:#b48ead;">import </span><span>{ </span><span style="color:#bf616a;">storageFor </span><span>} </span><span style="color:#b48ead;">from </span><span>'</span><span style="color:#a3be8c;">ember-local-storage</span><span>';
</span><span>
</span><span style="color:#b48ead;">export default </span><span style="color:#bf616a;">Controller</span><span>.</span><span style="color:#8fa1b3;">extend</span><span>({
</span><span> settings: </span><span style="color:#8fa1b3;">storageFor</span><span>('</span><span style="color:#a3be8c;">settings</span><span>'),
</span><span> wantsAnalytics: </span><span style="color:#d08770;">true</span><span>,
</span><span> </span><span style="color:#8fa1b3;">updateAnalytics</span><span>: </span><span style="color:#b48ead;">function</span><span>() {
</span><span> </span><span style="color:#bf616a;">this</span><span>.</span><span style="color:#96b5b4;">set</span><span>('</span><span style="color:#a3be8c;">settings.analytics</span><span>', </span><span style="color:#bf616a;">this</span><span>.</span><span style="color:#96b5b4;">get</span><span>('</span><span style="color:#a3be8c;">wantsAnalytics</span><span>'));
</span><span> }.</span><span style="color:#8fa1b3;">observes</span><span>('</span><span style="color:#a3be8c;">wantsAnalytics</span><span>')
</span><span>});
</span><span>
</span></code></pre>
<p>And that’s all there is to it! You can see this in action on your local environment by opening the developer console. Google Analytics is set to debug mode via Ember Metrics, so you can see when a page view is sent.</p>
Handling slug URLs in Ember and Phoenix2015-11-02T00:00:00+00:002015-11-02T00:00:00+00:00https://www.fullstackstanley.com/articles/handling-slug-urls-in-ember-and-phoenix/<p>I've recently started playing around with the PEEP stack (Postgres Elixir Ember Phoenix). One of the first things I wanted achieve is using SEO-friendly slugs instead of ids in Ember application's URLs.</p>
<span id="continue-reading"></span>
<p>This article presumes you know how to set up a fresh Ember project and a fresh Phoenix application as we will be diving head first into the code. </p>
<h3 id="version-info">Version info:</h3>
<p>These are the tools I'm using for this application.</p>
<ul>
<li>Node 4.2.1</li>
<li>Ember 1.13.8 / 2.0.2</li>
<li>Elixir 1.0.5</li>
<li>Phoenix 1.0.3</li>
<li>PostgreSQL 9.4.5</li>
</ul>
<h3 id="notes">Notes</h3>
<p>There is no special configuration required for each to work together. Although, I recommend launching Ember with the proxy argument as shown below.</p>
<pre data-lang="bash" style="background-color:#2b303b;color:#c0c5ce;" class="language-bash "><code class="language-bash" data-lang="bash"><span style="color:#bf616a;">ember</span><span> serve</span><span style="color:#bf616a;"> --proxy</span><span>=http://localhost:4000
</span></code></pre>
<p>This means you don't have to worry about Content Security Policy between Ember and your API.</p>
<p>If you haven't already, check out Maxwell Holder's <a href="http://maxwellholder.com/blog/build-a-blog-with-phoenix-and-ember">Build a Blog with Phoenix and Ember.js</a>. This article really helped me get started with using Ember and Phoenix together.</p>
<p>I'll be demonstrating how to set up 2 pages: a page with a list of products and a page for individual products which have a slug instead of an ID. This includes querying the slug on the API as well.</p>
<h2 id="show-me-the-code">Show me the code!</h2>
<p>The code is available on <a href="https://github.com/acoustep/ember-phoenix-slug-example">Github</a>.</p>
<p>Now on to the tutorial!</p>
<h2 id="setting-up-phoenix">Setting up Phoenix</h2>
<p>First run <code>mix ecto.create</code> to make sure the database is set up correctly.</p>
<p>Phoenix provders a really useful generator for APIs. Run the following command to get the cruft of the work done for you.</p>
<pre data-lang="bash" style="background-color:#2b303b;color:#c0c5ce;" class="language-bash "><code class="language-bash" data-lang="bash"><span style="color:#bf616a;">mix</span><span> phoenix.gen.json Product products name:string slug:string blurb:text preview:string featured:boolean
</span></code></pre>
<p>The above command generates the view, controller, migration and model files. </p>
<p>The fields include a name, a slug, a blurb for a short product description, preview which will be a link to an image and featured which is a boolean which states if the product is featured or not.</p>
<p>To make this accessible we need to add resource to the router.</p>
<pre data-lang="elixir" style="background-color:#2b303b;color:#c0c5ce;" class="language-elixir "><code class="language-elixir" data-lang="elixir"><span style="color:#65737e;"># web/router.ex
</span><span>scope "</span><span style="color:#a3be8c;">/api</span><span>", </span><span style="color:#ebcb8b;">Api </span><span style="color:#b48ead;">do
</span><span> pipe_through </span><span style="color:#a3be8c;">:api
</span><span> resources "</span><span style="color:#a3be8c;">/products</span><span>", </span><span style="color:#ebcb8b;">ProductController</span><span>, </span><span style="color:#d08770;">only: </span><span>[</span><span style="color:#a3be8c;">:index</span><span>, </span><span style="color:#a3be8c;">:show</span><span>]
</span><span style="color:#b48ead;">end
</span></code></pre>
<p><a href="https://github.com/acoustep/ember-phoenix-slug-example/commit/059687bb6634e58752e870c6681876ed55b95db1">Git Commit</a></p>
<p>We've added the API pipeline which we will be modifying shortly. For now, migrate the database in the terminal</p>
<pre data-lang="bash" style="background-color:#2b303b;color:#c0c5ce;" class="language-bash "><code class="language-bash" data-lang="bash"><span style="color:#bf616a;">mix</span><span> ecto.migrate
</span></code></pre>
<p>As of now the API works, however, it needs a couple of tweeks to be compatible with Ember and to support slugs.</p>
<h3 id="ember-compatible-api">Ember compatible API</h3>
<p>Phoenix wraps JSON objects and collections with the "data" attribute but Ember (currently in 2.1 and below) uses the model's singular name for objects and plural name for collections.</p>
<pre data-lang="javascript" style="background-color:#2b303b;color:#c0c5ce;" class="language-javascript "><code class="language-javascript" data-lang="javascript"><span style="color:#65737e;">// Phoenix currently
</span><span>{"</span><span style="color:#a3be8c;">data</span><span>":[]}
</span></code></pre>
<pre data-lang="javascript" style="background-color:#2b303b;color:#c0c5ce;" class="language-javascript "><code class="language-javascript" data-lang="javascript"><span style="color:#65737e;">// What Ember wants
</span><span>{"</span><span style="color:#a3be8c;">products</span><span>":[]} </span><span style="color:#65737e;">// Collection
</span><span>{"</span><span style="color:#a3be8c;">product</span><span>":[]} </span><span style="color:#65737e;">// Single Objects
</span></code></pre>
<p>Phoenix makes this really easy to change.</p>
<p>In <code>product_view.ex</code> change <code>data</code> in both the <code>index</code> and <code>show</code> <code>render</code> methods.</p>
<p>The <code>render</code> method which pattern matches <code>index.json</code> should change <code>data</code> to <code>products</code> and the <code>render</code> method which pattern matches <code>show.json</code> should change <code>data</code> to <code>product</code>.</p>
<pre data-lang="elixir" style="background-color:#2b303b;color:#c0c5ce;" class="language-elixir "><code class="language-elixir" data-lang="elixir"><span style="color:#65737e;"># web/views/product_view.ex
</span><span style="color:#b48ead;">defmodule </span><span style="color:#ebcb8b;">Api</span><span>.</span><span style="color:#ebcb8b;">ProductView </span><span style="color:#b48ead;">do
</span><span> </span><span style="color:#b48ead;">use </span><span style="color:#ebcb8b;">Api</span><span>.</span><span style="color:#ebcb8b;">Web</span><span>, </span><span style="color:#a3be8c;">:view
</span><span>
</span><span> </span><span style="color:#b48ead;">def </span><span style="color:#8fa1b3;">render</span><span>("</span><span style="color:#a3be8c;">index.json</span><span>", %{</span><span style="color:#d08770;">products:</span><span> products}) </span><span style="color:#b48ead;">do
</span><span> %{</span><span style="color:#d08770;">products:</span><span> render_many(products, </span><span style="color:#ebcb8b;">Api</span><span>.</span><span style="color:#ebcb8b;">ProductView</span><span>, "</span><span style="color:#a3be8c;">product.json</span><span>")}
</span><span> </span><span style="color:#b48ead;">end
</span><span>
</span><span> </span><span style="color:#b48ead;">def </span><span style="color:#8fa1b3;">render</span><span>("</span><span style="color:#a3be8c;">show.json</span><span>", %{</span><span style="color:#d08770;">product:</span><span> product}) </span><span style="color:#b48ead;">do
</span><span> %{</span><span style="color:#d08770;">product:</span><span> render_one(product, </span><span style="color:#ebcb8b;">Api</span><span>.</span><span style="color:#ebcb8b;">ProductView</span><span>, "</span><span style="color:#a3be8c;">product.json</span><span>")}
</span><span> </span><span style="color:#b48ead;">end
</span><span>
</span><span> </span><span style="color:#b48ead;">def </span><span style="color:#8fa1b3;">render</span><span>("</span><span style="color:#a3be8c;">product.json</span><span>", %{</span><span style="color:#d08770;">product:</span><span> product}) </span><span style="color:#b48ead;">do
</span><span> %{</span><span style="color:#d08770;">id:</span><span> product.id,
</span><span> </span><span style="color:#d08770;">name:</span><span> product.name,
</span><span> </span><span style="color:#d08770;">slug:</span><span> product.slug,
</span><span> </span><span style="color:#d08770;">blurb:</span><span> product.blurb,
</span><span> </span><span style="color:#d08770;">preview:</span><span> product.preview,
</span><span> </span><span style="color:#d08770;">featured:</span><span> product.featured,
</span><span> </span><span style="color:#b48ead;">end
</span><span style="color:#b48ead;">end
</span></code></pre>
<p><a href="https://github.com/acoustep/ember-phoenix-slug-example/commit/ad372e8f6588e104ceb23e083ca3f8af6eb5b672">Git commit</a></p>
<h3 id="just-add-slugs">Just add slugs</h3>
<p>To search for <code>slug</code> instead of the <code>id</code> primary key, we need to replace the <code>Repo.get</code> with a query that uses <code>Repo.one</code> or <code>Repo.one!</code> in the product controller - using the exclamation marked version will throw an error if nothing is found. I recommend this route as you can configure Ember to redirect elsewhere in this situation.</p>
<pre data-lang="elixir" style="background-color:#2b303b;color:#c0c5ce;" class="language-elixir "><code class="language-elixir" data-lang="elixir"><span style="color:#b48ead;">def </span><span style="color:#8fa1b3;">show</span><span>(conn, %{"</span><span style="color:#a3be8c;">id</span><span>" => slug}) </span><span style="color:#b48ead;">do
</span><span> query = from p in </span><span style="color:#ebcb8b;">Product</span><span>,
</span><span> </span><span style="color:#d08770;">where:</span><span> p.slug == ^</span><span style="color:#bf616a;">slug</span><span>,
</span><span> </span><span style="color:#d08770;">select:</span><span> p
</span><span> product = </span><span style="color:#ebcb8b;">Repo</span><span>.one!(query)
</span><span> render(conn, "</span><span style="color:#a3be8c;">show.json</span><span>", </span><span style="color:#d08770;">product:</span><span> product)
</span><span style="color:#b48ead;">end
</span></code></pre>
<p><a href="https://github.com/acoustep/ember-phoenix-slug-example/commit/d45d0fb8b9eff63784af5e11fd836e3b87858458">Git commit</a></p>
<p>On line 1 <code>id</code> has been altered to say <code>slug</code>, this is just so we're clear that we're dealing with a slug. The map key is still <code>id</code>, though, as the product resource route added previously is set up to pattern match for <code>id</code>. To change it to <code>slug</code> requires adding a separate route.</p>
<p>Lines 2 to 4 is the query to find the slug in the database.</p>
<p>Line 5 uses <code>Repo.one!</code> to fetch the first match for the query or throw an error.</p>
<h3 id="cors-in-production">CORS in production</h3>
<p>Although using ember serve --proxy will solve this issue in development, it's worth adding <a href="https://github.com/whatyouhide/corsica">Corsica</a> for production environments.</p>
<p>After following the installation instructions modify the API pipeline in the router:</p>
<pre data-lang="elixir" style="background-color:#2b303b;color:#c0c5ce;" class="language-elixir "><code class="language-elixir" data-lang="elixir"><span> pipeline </span><span style="color:#a3be8c;">:api </span><span style="color:#b48ead;">do
</span><span> plug </span><span style="color:#a3be8c;">:accepts</span><span>, ["</span><span style="color:#a3be8c;">json</span><span>"]
</span><span> plug </span><span style="color:#ebcb8b;">Corsica</span><span>, </span><span style="color:#d08770;">origins: </span><span>["</span><span style="color:#a3be8c;">localhost:4200</span><span>", "</span><span style="color:#a3be8c;">example.com</span><span>"]
</span><span> </span><span style="color:#b48ead;">end
</span></code></pre>
<p><a href="https://github.com/acoustep/ember-phoenix-slug-example/commit/2fcfb08479549e451039b586cc64a2970289d3ce">Git commit</a></p>
<p>This will modify response headers to show which sources can load the API.</p>
<p>Make sure that you restart your Phoenix server after installing Corsica.</p>
<pre data-lang="bash" style="background-color:#2b303b;color:#c0c5ce;" class="language-bash "><code class="language-bash" data-lang="bash"><span style="color:#bf616a;">mix</span><span> phoenix.server
</span></code></pre>
<p>Before you continue to the Ember section make sure you have some data in the database! For this example I've used a lorem ipsum generator and <a href="http://www.fillmurray.com/">Fill Murray</a> for the preview column.</p>
<h2 id="ember">Ember</h2>
<p>Let's use Twitter Bootstrap to make the application look presentable.</p>
<pre data-lang="bash" style="background-color:#2b303b;color:#c0c5ce;" class="language-bash "><code class="language-bash" data-lang="bash"><span style="color:#bf616a;">ember</span><span> install ember-bootstrap
</span></code></pre>
<p>Note that because I have named my app "App" I have had to rename my <code>app.css</code> to <code>style.css</code>.</p>
<p>Modify the application template to use the bootstrap grid</p>
<pre data-lang="hbs" style="background-color:#2b303b;color:#c0c5ce;" class="language-hbs "><code class="language-hbs" data-lang="hbs"><span style="color:#65737e;">{{!-- app/templates/application.hbs --}}
</span><span><</span><span style="color:#bf616a;">div </span><span style="color:#d08770;">class=</span><span>"</span><span style="color:#a3be8c;">container</span><span>">
</span><span> <</span><span style="color:#bf616a;">div </span><span style="color:#d08770;">class=</span><span>"</span><span style="color:#a3be8c;">row</span><span>">
</span><span> <</span><span style="color:#bf616a;">div </span><span style="color:#d08770;">class=</span><span>"</span><span style="color:#a3be8c;">col-xs-12</span><span>">
</span><span> <</span><span style="color:#bf616a;">h1 </span><span style="color:#8fa1b3;">id</span><span>="</span><span style="color:#a3be8c;">title</span><span>">Ember Phoenix Slugs</</span><span style="color:#bf616a;">h1</span><span>>
</span><span> </</span><span style="color:#bf616a;">div</span><span>>
</span><span> </</span><span style="color:#bf616a;">div</span><span>>
</span><span> <</span><span style="color:#bf616a;">div </span><span style="color:#d08770;">class=</span><span>"</span><span style="color:#a3be8c;">row</span><span>">
</span><span> <</span><span style="color:#bf616a;">div </span><span style="color:#d08770;">class=</span><span>"</span><span style="color:#a3be8c;">col-xs-12</span><span>">
</span><span> {{</span><span style="color:#bf616a;">outlet</span><span>}}
</span><span> </</span><span style="color:#bf616a;">div</span><span>>
</span><span> </</span><span style="color:#bf616a;">div</span><span>>
</span><span></</span><span style="color:#bf616a;">div</span><span>>
</span></code></pre>
<p><a href="https://github.com/acoustep/ember-phoenix-slug-example/commit/044ee7d5c3622e7f2dca25c7fe47e45116a0ad96">Git commit</a></p>
<p>Make sure you restart the Ember server after installing the addon.</p>
<pre data-lang="bash" style="background-color:#2b303b;color:#c0c5ce;" class="language-bash "><code class="language-bash" data-lang="bash"><span style="color:#bf616a;">ember</span><span> serve</span><span style="color:#bf616a;"> --proxy</span><span>=http://localhost:4000
</span></code></pre>
<p>Before we can hook up Ember to the API, we need to generate the application adapter to add the 'api' namespace that we've set up in Phoenix.</p>
<pre data-lang="bash" style="background-color:#2b303b;color:#c0c5ce;" class="language-bash "><code class="language-bash" data-lang="bash"><span style="color:#bf616a;">ember</span><span> g adapter application
</span></code></pre>
<p>Open up the newly created adapter file and add the namespace property.</p>
<pre data-lang="js" style="background-color:#2b303b;color:#c0c5ce;" class="language-js "><code class="language-js" data-lang="js"><span style="color:#65737e;">// app/adapters/application.js
</span><span style="color:#b48ead;">import </span><span style="color:#bf616a;">DS </span><span style="color:#b48ead;">from </span><span>'</span><span style="color:#a3be8c;">ember-data</span><span>';
</span><span>
</span><span style="color:#b48ead;">export default </span><span style="color:#bf616a;">DS</span><span>.</span><span style="color:#bf616a;">RESTAdapter</span><span>.</span><span style="color:#8fa1b3;">extend</span><span>({
</span><span> namespace: '</span><span style="color:#a3be8c;">api</span><span>'
</span><span>});
</span></code></pre>
<p><a href="https://github.com/acoustep/ember-phoenix-slug-example/commit/2e85a9e40fd40ec1d27c9ab6d79ce564c21bf5bd">Git commit</a></p>
<p>Now we can set up the model we'll use to connect to the API</p>
<pre data-lang="bash" style="background-color:#2b303b;color:#c0c5ce;" class="language-bash "><code class="language-bash" data-lang="bash"><span style="color:#bf616a;">ember</span><span> g model product
</span></code></pre>
<pre data-lang="js" style="background-color:#2b303b;color:#c0c5ce;" class="language-js "><code class="language-js" data-lang="js"><span style="color:#65737e;">// app/models/product.js
</span><span style="color:#b48ead;">import </span><span style="color:#bf616a;">DS </span><span style="color:#b48ead;">from </span><span>'</span><span style="color:#a3be8c;">ember-data</span><span>';
</span><span>
</span><span style="color:#b48ead;">export default </span><span style="color:#bf616a;">DS</span><span>.</span><span style="color:#bf616a;">Model</span><span>.</span><span style="color:#8fa1b3;">extend</span><span>({
</span><span> name: </span><span style="color:#bf616a;">DS</span><span>.</span><span style="color:#8fa1b3;">attr</span><span>(),
</span><span> slug: </span><span style="color:#bf616a;">DS</span><span>.</span><span style="color:#8fa1b3;">attr</span><span>(),
</span><span> blurb: </span><span style="color:#bf616a;">DS</span><span>.</span><span style="color:#8fa1b3;">attr</span><span>(),
</span><span> preview: </span><span style="color:#bf616a;">DS</span><span>.</span><span style="color:#8fa1b3;">attr</span><span>(),
</span><span>});
</span></code></pre>
<p>Generate the index route to connect to the product model.</p>
<pre data-lang="bash" style="background-color:#2b303b;color:#c0c5ce;" class="language-bash "><code class="language-bash" data-lang="bash"><span style="color:#bf616a;">ember</span><span> g route index
</span></code></pre>
<pre data-lang="js" style="background-color:#2b303b;color:#c0c5ce;" class="language-js "><code class="language-js" data-lang="js"><span style="color:#65737e;">// app/routes/index.js
</span><span style="color:#b48ead;">import </span><span style="color:#bf616a;">Ember </span><span style="color:#b48ead;">from </span><span>'</span><span style="color:#a3be8c;">ember</span><span>';
</span><span>
</span><span style="color:#b48ead;">export default </span><span style="color:#bf616a;">Ember</span><span>.</span><span style="color:#bf616a;">Route</span><span>.</span><span style="color:#8fa1b3;">extend</span><span>({
</span><span> </span><span style="color:#8fa1b3;">model</span><span>: </span><span style="color:#b48ead;">function</span><span>() {
</span><span> </span><span style="color:#b48ead;">return </span><span style="color:#bf616a;">this</span><span>.</span><span style="color:#bf616a;">store</span><span>.</span><span style="color:#8fa1b3;">findAll</span><span>('</span><span style="color:#a3be8c;">product</span><span>');
</span><span> }
</span><span>});
</span></code></pre>
<p>Here is the template for the index. It loops through all the products and links to them in the image and headings.</p>
<pre data-lang="hbs" style="background-color:#2b303b;color:#c0c5ce;" class="language-hbs "><code class="language-hbs" data-lang="hbs"><span style="color:#65737e;">{{!-- app/templates/index.hbs --}}
</span><span><</span><span style="color:#bf616a;">h2</span><span>>Products</</span><span style="color:#bf616a;">h2</span><span>>
</span><span><</span><span style="color:#bf616a;">div </span><span style="color:#d08770;">class=</span><span>"</span><span style="color:#a3be8c;">row</span><span>">
</span><span> {{#each </span><span style="color:#bf616a;">model </span><span>as |</span><span style="color:#bf616a;">product</span><span>|}}
</span><span> <</span><span style="color:#bf616a;">div </span><span style="color:#d08770;">class=</span><span>"</span><span style="color:#a3be8c;">col-xs-3</span><span>">
</span><span> {{#link-to '</span><span style="color:#a3be8c;">product</span><span>' </span><span style="color:#bf616a;">product</span><span>}}
</span><span> <</span><span style="color:#bf616a;">img </span><span style="color:#d08770;">src=</span><span>{{</span><span style="color:#bf616a;">product.preview</span><span>}} </span><span style="color:#d08770;">class=</span><span>"</span><span style="color:#a3be8c;">product--image</span><span>">
</span><span> <</span><span style="color:#bf616a;">h3</span><span>>{{</span><span style="color:#bf616a;">product.name</span><span>}}</</span><span style="color:#bf616a;">h3</span><span>>
</span><span> {{/link-to}}
</span><span> <</span><span style="color:#bf616a;">p</span><span>>{{</span><span style="color:#bf616a;">product.blurb</span><span>}}</</span><span style="color:#bf616a;">p</span><span>>
</span><span> </</span><span style="color:#bf616a;">div</span><span>>
</span><span> {{/each}}
</span><span></</span><span style="color:#bf616a;">div</span><span>>
</span></code></pre>
<p><a href="https://github.com/acoustep/ember-phoenix-slug-example/commit/c3db53c239aed1f676db8c8c469d3e2535f8064a">Git commit</a></p>
<p>Note that if you're using Ember 2 or onwards you can remove the curly brackets around the image source value.</p>
<p>Next we need to generate the product route.</p>
<h3 id="product-page">Product page</h3>
<p>As of now Ember will throw an error due to using a product route which doesn't exist yet. Let's fix that.</p>
<pre data-lang="bash" style="background-color:#2b303b;color:#c0c5ce;" class="language-bash "><code class="language-bash" data-lang="bash"><span style="color:#bf616a;">ember</span><span> g route product
</span></code></pre>
<pre data-lang="js" style="background-color:#2b303b;color:#c0c5ce;" class="language-js "><code class="language-js" data-lang="js"><span style="color:#65737e;">// app/routes/product.js
</span><span style="color:#b48ead;">import </span><span style="color:#bf616a;">Ember </span><span style="color:#b48ead;">from </span><span>'</span><span style="color:#a3be8c;">ember</span><span>';
</span><span>
</span><span style="color:#b48ead;">export default </span><span style="color:#bf616a;">Ember</span><span>.</span><span style="color:#bf616a;">Route</span><span>.</span><span style="color:#8fa1b3;">extend</span><span>({
</span><span> </span><span style="color:#8fa1b3;">model</span><span>: </span><span style="color:#b48ead;">function</span><span>(</span><span style="color:#bf616a;">params</span><span>) {
</span><span> </span><span style="color:#bf616a;">this</span><span>.</span><span style="color:#96b5b4;">set</span><span>('</span><span style="color:#a3be8c;">product</span><span>', </span><span style="color:#bf616a;">this</span><span>.</span><span style="color:#8fa1b3;">modelFor</span><span>('</span><span style="color:#a3be8c;">product</span><span>'));
</span><span> </span><span style="color:#b48ead;">return </span><span style="color:#bf616a;">this</span><span>.</span><span style="color:#bf616a;">store</span><span>.</span><span style="color:#96b5b4;">find</span><span>('</span><span style="color:#a3be8c;">product</span><span>', </span><span style="color:#bf616a;">params</span><span>.</span><span style="color:#bf616a;">product_slug</span><span>);
</span><span> },
</span><span> </span><span style="color:#8fa1b3;">serialize</span><span>: </span><span style="color:#b48ead;">function</span><span>(</span><span style="color:#bf616a;">model</span><span>, </span><span style="color:#bf616a;">params</span><span>) {
</span><span> </span><span style="color:#b48ead;">return </span><span>{ product_slug: </span><span style="color:#bf616a;">model</span><span>.</span><span style="color:#96b5b4;">get</span><span>('</span><span style="color:#a3be8c;">slug</span><span>') };
</span><span> }
</span><span>});
</span></code></pre>
<p>On line 6 we try to find the product by the <code>product_slug</code>.</p>
<p>The <code>serialize</code> method needs to be implemented when an attribute other than <code>id</code> is used for the primary key. We are telling the Ember that when a product object is passed through the <code>link-to</code> method to use the <code>slug</code> instead. <code>product_slug</code> will match in the route we are creating below (On line 10):</p>
<pre data-lang="js" style="background-color:#2b303b;color:#c0c5ce;" class="language-js "><code class="language-js" data-lang="js"><span style="color:#65737e;">// app/router.js
</span><span style="color:#b48ead;">import </span><span style="color:#bf616a;">Ember </span><span style="color:#b48ead;">from </span><span>'</span><span style="color:#a3be8c;">ember</span><span>';
</span><span style="color:#b48ead;">import </span><span style="color:#bf616a;">config </span><span style="color:#b48ead;">from </span><span>'</span><span style="color:#a3be8c;">./config/environment</span><span>';
</span><span>
</span><span style="color:#b48ead;">var </span><span style="color:#bf616a;">Router </span><span>= </span><span style="color:#bf616a;">Ember</span><span>.</span><span style="color:#bf616a;">Router</span><span>.</span><span style="color:#8fa1b3;">extend</span><span>({
</span><span> location: </span><span style="color:#bf616a;">config</span><span>.</span><span style="color:#bf616a;">locationType
</span><span>});
</span><span>
</span><span style="color:#bf616a;">Router</span><span>.</span><span style="color:#8fa1b3;">map</span><span>(</span><span style="color:#b48ead;">function</span><span>() {
</span><span> </span><span style="color:#bf616a;">this</span><span>.</span><span style="color:#8fa1b3;">route</span><span>('</span><span style="color:#a3be8c;">product</span><span>', {path: '</span><span style="color:#a3be8c;">/products/:product_slug</span><span>'});
</span><span>});
</span><span style="color:#b48ead;">export default </span><span style="color:#bf616a;">Router</span><span>;
</span></code></pre>
<p>The template:</p>
<pre data-lang="hbs" style="background-color:#2b303b;color:#c0c5ce;" class="language-hbs "><code class="language-hbs" data-lang="hbs"><span style="color:#65737e;">{{!-- app/templates/index.hbs --}}
</span><span><</span><span style="color:#bf616a;">h2</span><span>>{{</span><span style="color:#bf616a;">model.name</span><span>}}</</span><span style="color:#bf616a;">h2</span><span>>
</span><span><</span><span style="color:#bf616a;">img </span><span style="color:#d08770;">src=</span><span>{{</span><span style="color:#bf616a;">model.preview</span><span>}} </span><span style="color:#d08770;">class=</span><span>"</span><span style="color:#a3be8c;">product--image</span><span>">
</span><span><</span><span style="color:#bf616a;">h3</span><span>>{{</span><span style="color:#bf616a;">model.name</span><span>}}</</span><span style="color:#bf616a;">h3</span><span>>
</span><span><</span><span style="color:#bf616a;">p</span><span>>{{</span><span style="color:#bf616a;">model.blurb</span><span>}}</</span><span style="color:#bf616a;">p</span><span>>
</span><span>{{</span><span style="color:#bf616a;">link-to </span><span>'</span><span style="color:#a3be8c;">Back</span><span>' '</span><span style="color:#a3be8c;">index</span><span>' </span><span style="color:#bf616a;">class</span><span style="color:#d08770;">=</span><span>"</span><span style="color:#a3be8c;">btn btn-default</span><span>"}}
</span></code></pre>
<p><a href="https://github.com/acoustep/ember-phoenix-slug-example/commit/686ee06580dc349143b1c72e105356c12ec3916c">Git commit</a></p>
<h3 id="error-page">Error page</h3>
<p>If the slug can't be found then Phoenix will return a 404 error. The easiest way to handle this in Ember is create a <code>product-error</code> template which Ember will show automatically.</p>
<pre data-lang="hbs" style="background-color:#2b303b;color:#c0c5ce;" class="language-hbs "><code class="language-hbs" data-lang="hbs"><span style="color:#65737e;">{{!-- app/templates/product-error.hbs --}}
</span><span><</span><span style="color:#bf616a;">h2</span><span>>Whoops</</span><span style="color:#bf616a;">h2</span><span>>
</span><span><</span><span style="color:#bf616a;">p</span><span>>This page could not be found.</</span><span style="color:#bf616a;">p</span><span>>
</span><span>{{</span><span style="color:#bf616a;">link-to </span><span>'</span><span style="color:#a3be8c;">Back</span><span>' '</span><span style="color:#a3be8c;">index</span><span>' </span><span style="color:#bf616a;">class</span><span style="color:#d08770;">=</span><span>"</span><span style="color:#a3be8c;">btn btn-default</span><span>"}}
</span></code></pre>
<p><a href="https://github.com/acoustep/ember-phoenix-slug-example/commit/4aac158851145c73a24b47ffedf3baae46d37ffd">Git commit</a></p>
<h1 id="setting-slug-as-the-primary-key">Setting slug as the primary key</h1>
<p>Right now the application works, however, while refreshing an individual product page if you Ember tries to find a product with an <code>id</code> of <code>slug</code>, then a <code>slug</code> of <code>slug</code>. This results in two separate objects rather than one.</p>
<p>This sounds rather confusing and is best illustrated through Ember inspector:</p>
<p><img src="https://i.imgur.com/2l8qD9T.png" alt="Extra object" title="Extra object" /></p>
<p>To get around this we can change the product model primary key to slug by generating a product serializer and updating the <code>primaryKey</code> property.</p>
<pre data-lang="bash" style="background-color:#2b303b;color:#c0c5ce;" class="language-bash "><code class="language-bash" data-lang="bash"><span style="color:#bf616a;">ember</span><span> g serializer product
</span></code></pre>
<pre data-lang="js" style="background-color:#2b303b;color:#c0c5ce;" class="language-js "><code class="language-js" data-lang="js"><span style="color:#65737e;">// app/serializers/product.js
</span><span style="color:#b48ead;">import </span><span style="color:#bf616a;">DS </span><span style="color:#b48ead;">from </span><span>'</span><span style="color:#a3be8c;">ember-data</span><span>';
</span><span>
</span><span style="color:#b48ead;">export default </span><span style="color:#bf616a;">DS</span><span>.</span><span style="color:#bf616a;">RESTSerializer</span><span>.</span><span style="color:#8fa1b3;">extend</span><span>({
</span><span> primaryKey: '</span><span style="color:#a3be8c;">slug</span><span>'
</span><span>});
</span></code></pre>
<p>By doing this the slug attribute on each product object gets moved to the id field and leaves slug as undefined. We will need to update the product route to reflect this change.</p>
<pre data-lang="js" style="background-color:#2b303b;color:#c0c5ce;" class="language-js "><code class="language-js" data-lang="js"><span style="color:#65737e;">// app/routes/product.js
</span><span style="color:#b48ead;">import </span><span style="color:#bf616a;">Ember </span><span style="color:#b48ead;">from </span><span>'</span><span style="color:#a3be8c;">ember</span><span>';
</span><span>
</span><span style="color:#b48ead;">export default </span><span style="color:#bf616a;">Ember</span><span>.</span><span style="color:#bf616a;">Route</span><span>.</span><span style="color:#8fa1b3;">extend</span><span>({
</span><span> </span><span style="color:#8fa1b3;">model</span><span>: </span><span style="color:#b48ead;">function</span><span>(</span><span style="color:#bf616a;">params</span><span>) {
</span><span> </span><span style="color:#bf616a;">this</span><span>.</span><span style="color:#96b5b4;">set</span><span>('</span><span style="color:#a3be8c;">product</span><span>', </span><span style="color:#bf616a;">this</span><span>.</span><span style="color:#8fa1b3;">modelFor</span><span>('</span><span style="color:#a3be8c;">product</span><span>'));
</span><span> </span><span style="color:#b48ead;">return </span><span style="color:#bf616a;">this</span><span>.</span><span style="color:#bf616a;">store</span><span>.</span><span style="color:#96b5b4;">find</span><span>('</span><span style="color:#a3be8c;">product</span><span>', </span><span style="color:#bf616a;">params</span><span>.</span><span style="color:#bf616a;">product_slug</span><span>);
</span><span> },
</span><span> </span><span style="color:#8fa1b3;">serialize</span><span>: </span><span style="color:#b48ead;">function</span><span>(</span><span style="color:#bf616a;">model</span><span>, </span><span style="color:#bf616a;">params</span><span>) {
</span><span> </span><span style="color:#b48ead;">return </span><span>{ product_slug: </span><span style="color:#bf616a;">model</span><span>.</span><span style="color:#96b5b4;">get</span><span>('</span><span style="color:#a3be8c;">id</span><span>') };
</span><span> }
</span><span>});
</span></code></pre>
<p><a href="https://github.com/acoustep/ember-phoenix-slug-example/commit/871e667e8cf84605f3bb1b4d2bb5a7dca914a614">Git commit</a></p>
<p>Notice now on line 10 how <code>model.get()</code> is looking for id rather than <code>slug</code>.</p>
<p>Alternatively, we could remove the <code>serialize</code> method and update the <code>router.js</code> to look for <code>:id</code> rather than <code>:product_slug</code>.</p>
<h2 id="preview">Preview</h2>
<p><img src="https://i.imgur.com/Hlj8Ef5.png" alt="Product listing page" title="Product listing page" /></p>
<p><img src="https://i.imgur.com/yflOrbM.png" alt="Product page" title="Product page" /></p>
<p><img src="https://i.imgur.com/iwpxAif.png" alt="Whoops 404 page" title="Whoops 404 page" /></p>
<h2 id="summary">Summary</h2>
<p>In this tutorial we have gone through how to set up <code>index</code> and <code>show</code> methods in a Phoenix application to work with URL slugs.</p>
<p>We have connected this API to an Ember application which can view product listings and link to individual product pages.</p>
Realtime chat with Laravel, Ember JS and Pusher2015-03-29T00:00:00+00:002015-03-29T00:00:00+00:00https://www.fullstackstanley.com/articles/realtime-chat-with-laravel-ember-js-and-pusher/<p>I've been hearing a lot of noise about Firebase recently. Lots of good noise. When I first started making this article I wanted to make use of Firehose which is an open source alternative to Firebase. </p>
<span id="continue-reading"></span>
<p>Setting up the API with Laravel was a breeze. The only stumbling block was with my Ubuntu Vagrant box which installed a version of Redis which was too old (Firehose requries 2.6+ where as Ubuntu's package manager only has 2.2). This was a pretty simple to fix and if you <em>are</em> interested in Firehose then you can find instructions for getting the latest Redis <a href="http://codecuriosity.com/blog/2013/10/29/install-redis-on-ubuntu/">here</a>.</p>
<p>Anyway I started building my Ember application and I hit a brick wall. Firehose touts Ember support out of the box but I can see literally no documentation on how to get started with it.</p>
<p>I decided to switch to Pusher which I'm already familiar with. Unfortunately Pusher isn't open source but it does have a reasonable free package. This allowed me to continue the use of my Laravel application where as switching to Firebase would not. Also, there is already a pretty amazing Firebase Ember tutorial <a href="https://www.firebase.com/blog/2015-03-13-ember-cli-in-9-minutes.html">here</a> so it seems pointless to do the same thing.</p>
<p>This article will show you a very bare bones chat system with Laravel 5, Ember JS 1.10 and Pusher.</p>
<span id="continue-reading"></span><h2 id="laravel">Laravel</h2>
<p>Start off by making a new Laravel application</p>
<pre data-lang="bash" style="background-color:#2b303b;color:#c0c5ce;" class="language-bash "><code class="language-bash" data-lang="bash"><span style="color:#bf616a;">laravel</span><span> new realtimechat
</span></code></pre>
<p>For this application you'll use 2 packages: <a href="https://github.com/laracasts/Laravel-5-Generators-Extended">laracasts/Laravel-5-Generators-Extended</a> and <a href="https://github.com/vinkla/pusher">vinkla/pusher</a>.</p>
<p>Install the two packages</p>
<pre data-lang="bash" style="background-color:#2b303b;color:#c0c5ce;" class="language-bash "><code class="language-bash" data-lang="bash"><span style="color:#bf616a;">composer</span><span> require laracasts/generators</span><span style="color:#bf616a;"> --dev
</span><span style="color:#bf616a;">composer</span><span> require vinkla/pusher:</span><span style="color:#bf616a;">~</span><span>1.0
</span></code></pre>
<p>In <code>app/Providers/AppServiceProvider.php</code> add the generator service provider as suggested in the documentation.</p>
<pre data-lang="php" style="background-color:#2b303b;color:#c0c5ce;" class="language-php "><code class="language-php" data-lang="php"><span>if ($this->app->environment() == 'local') {
</span><span> $this->app->register('Laracasts\Generators\GeneratorsServiceProvider');
</span><span>}
</span></code></pre>
<p>In <code>config/app.php</code> Add the Pusher service provider. Do not add the facade as at the time of writing there is an issue with it.</p>
<pre data-lang="php" style="background-color:#2b303b;color:#c0c5ce;" class="language-php "><code class="language-php" data-lang="php"><span>'Vinkla\Pusher\PusherServiceProvider'
</span></code></pre>
<p>Create a new migration for a table called messages</p>
<pre style="background-color:#2b303b;color:#c0c5ce;"><code><span>php artisan make:migration:schema create_messages_table --schema="name:string, body:string"
</span></code></pre>
<p>The first field, <code>name</code>, will be the user's name. The second field, <code>body</code>, will be the content of the message. You might be tempted to rename <code>body</code> to <code>content</code> (as I was). I recommend that you don't because Ember's controllers and templates set <code>content</code> to the current model which makes things unnecessaryly confusing.</p>
<p>If you haven't configured your database settings then now is the time. I'm using Sqlite for this application which works perfectly well. Run the migration afterwards.</p>
<pre data-lang="bash" style="background-color:#2b303b;color:#c0c5ce;" class="language-bash "><code class="language-bash" data-lang="bash"><span style="color:#bf616a;">php</span><span> artisan migrate
</span></code></pre>
<p>You will need to remove the global CSRF token protection provided by Laravel. If your app has other non-API routes then I recommend wrapping them in <code>Route::group</code> with the CSRF middleware. You can remove the global middleware in <code>app/Http/Kernal.php</code> by removing the <code>App\Http\Middleware\VerifyCSRFToken</code> line.</p>
<p>At this point make sure you have signed up at <a href="http://pusher.com">Pusher</a> and create your first application. </p>
<p>In your terminal publish the Pusher configuration file with this command</p>
<pre data-lang="bash" style="background-color:#2b303b;color:#c0c5ce;" class="language-bash "><code class="language-bash" data-lang="bash"><span style="color:#bf616a;">php</span><span> artisan vendor:publish
</span></code></pre>
<p>Open up <code>app/config/pusher.php</code> and add your <code>auth_key</code>, <code>secret</code> and <code>app_id</code> to the <code>main</code> array. These can be found on your Pusher application's dashboard.</p>
<p>In <code>app/Http/routes.php</code> add a new resource route.</p>
<pre data-lang="php" style="background-color:#2b303b;color:#c0c5ce;" class="language-php "><code class="language-php" data-lang="php"><span style="color:#ab7967;"><?php
</span><span style="color:#ebcb8b;">Route</span><span>::</span><span style="color:#bf616a;">resource</span><span>('</span><span style="color:#a3be8c;">messages</span><span>', '</span><span style="color:#a3be8c;">MessagesController</span><span>', ['</span><span style="color:#a3be8c;">only</span><span>' => ['</span><span style="color:#a3be8c;">index</span><span>', '</span><span style="color:#a3be8c;">store</span><span>', '</span><span style="color:#a3be8c;">show</span><span>']]);
</span></code></pre>
<p>You'll only be using <code>index</code> and <code>store</code> but it's worth defining <code>show</code> incase Ember tries to fetch a single row.</p>
<p>Now create the controller</p>
<pre data-lang="bash" style="background-color:#2b303b;color:#c0c5ce;" class="language-bash "><code class="language-bash" data-lang="bash"><span style="color:#bf616a;">php</span><span> artisan make:controller MessagesController
</span></code></pre>
<p>Open up <code>app/Http/Controllers/MessagesController.php</code> and update the code as follows:</p>
<pre data-lang="php" style="background-color:#2b303b;color:#c0c5ce;" class="language-php "><code class="language-php" data-lang="php"><span style="color:#ab7967;"><?php </span><span style="color:#d08770;">namespace </span><span>App\Http\</span><span style="color:#d08770;">Controllers</span><span>;
</span><span>
</span><span style="color:#b48ead;">use </span><span>App\Http\</span><span style="color:#ebcb8b;">Requests</span><span>;
</span><span style="color:#b48ead;">use </span><span>App\Http\Controllers\</span><span style="color:#ebcb8b;">Controller</span><span>;
</span><span style="color:#b48ead;">use </span><span>App\</span><span style="color:#ebcb8b;">Message</span><span>;
</span><span>
</span><span style="color:#b48ead;">use </span><span>Illuminate\Http\</span><span style="color:#ebcb8b;">Request</span><span>;
</span><span style="color:#b48ead;">use </span><span style="color:#ebcb8b;">Input</span><span>;
</span><span style="color:#b48ead;">use </span><span style="color:#ebcb8b;">Response</span><span>;
</span><span style="color:#b48ead;">use </span><span>GuzzleHttp\</span><span style="color:#ebcb8b;">Client</span><span>;
</span><span style="color:#b48ead;">use </span><span>Vinkla\Pusher\</span><span style="color:#ebcb8b;">PusherManager</span><span>;
</span><span>
</span><span>
</span><span style="color:#b48ead;">class </span><span style="color:#ebcb8b;">MessagesController </span><span style="color:#b48ead;">extends </span><span style="color:#a3be8c;">Controller </span><span style="color:#eff1f5;">{
</span><span style="color:#eff1f5;">
</span><span style="color:#eff1f5;"> </span><span style="color:#b48ead;">protected </span><span>$</span><span style="color:#bf616a;">message</span><span style="color:#eff1f5;">;
</span><span style="color:#eff1f5;"> </span><span style="color:#b48ead;">protected </span><span>$</span><span style="color:#bf616a;">pusher</span><span style="color:#eff1f5;">;
</span><span style="color:#eff1f5;">
</span><span style="color:#eff1f5;"> </span><span style="color:#b48ead;">public function </span><span style="color:#96b5b4;">__construct</span><span style="color:#eff1f5;">(</span><span style="color:#ebcb8b;">Message </span><span>$</span><span style="color:#bf616a;">message</span><span style="color:#eff1f5;">, </span><span style="color:#ebcb8b;">PusherManager </span><span>$</span><span style="color:#bf616a;">pusher</span><span style="color:#eff1f5;">)
</span><span style="color:#eff1f5;"> {
</span><span style="color:#eff1f5;"> </span><span>$</span><span style="color:#bf616a;">this</span><span style="color:#eff1f5;">-></span><span style="color:#bf616a;">message </span><span>= $</span><span style="color:#bf616a;">message</span><span style="color:#eff1f5;">;
</span><span style="color:#eff1f5;"> </span><span>$</span><span style="color:#bf616a;">this</span><span style="color:#eff1f5;">-></span><span style="color:#bf616a;">pusher </span><span>= $</span><span style="color:#bf616a;">pusher</span><span style="color:#eff1f5;">;
</span><span style="color:#eff1f5;"> }
</span><span style="color:#eff1f5;">
</span><span style="color:#eff1f5;"> </span><span style="color:#65737e;">/**
</span><span style="color:#65737e;"> * Display a listing of the resource.
</span><span style="color:#65737e;"> *
</span><span style="color:#65737e;"> * </span><span style="color:#b48ead;">@return</span><span style="color:#65737e;"> Response
</span><span style="color:#65737e;"> */
</span><span style="color:#eff1f5;"> </span><span style="color:#b48ead;">public function </span><span style="color:#8fa1b3;">index</span><span style="color:#eff1f5;">()
</span><span style="color:#eff1f5;"> {
</span><span style="color:#eff1f5;"> </span><span>$</span><span style="color:#bf616a;">messages </span><span>= $</span><span style="color:#bf616a;">this</span><span style="color:#eff1f5;">-></span><span style="color:#bf616a;">message</span><span style="color:#eff1f5;">-></span><span style="color:#bf616a;">orderBy</span><span style="color:#eff1f5;">(</span><span>'</span><span style="color:#a3be8c;">id</span><span>'</span><span style="color:#eff1f5;">, </span><span>'</span><span style="color:#a3be8c;">desc</span><span>'</span><span style="color:#eff1f5;">)-></span><span style="color:#bf616a;">take</span><span style="color:#eff1f5;">(</span><span style="color:#d08770;">5</span><span style="color:#eff1f5;">)-></span><span style="color:#bf616a;">get</span><span style="color:#eff1f5;">();
</span><span style="color:#eff1f5;">
</span><span style="color:#eff1f5;"> </span><span style="color:#b48ead;">return </span><span style="color:#ebcb8b;">Response</span><span style="color:#eff1f5;">::</span><span style="color:#bf616a;">json</span><span style="color:#eff1f5;">([</span><span>'</span><span style="color:#a3be8c;">messages</span><span>' => $</span><span style="color:#bf616a;">messages</span><span style="color:#eff1f5;">-></span><span style="color:#bf616a;">toArray</span><span style="color:#eff1f5;">()]);
</span><span style="color:#eff1f5;"> }
</span><span style="color:#eff1f5;">
</span><span style="color:#eff1f5;"> </span><span style="color:#65737e;">/**
</span><span style="color:#65737e;"> * Store a newly created resource in storage.
</span><span style="color:#65737e;"> *
</span><span style="color:#65737e;"> * </span><span style="color:#b48ead;">@return</span><span style="color:#65737e;"> Response
</span><span style="color:#65737e;"> */
</span><span style="color:#eff1f5;"> </span><span style="color:#b48ead;">public function </span><span style="color:#8fa1b3;">store</span><span style="color:#eff1f5;">()
</span><span style="color:#eff1f5;"> {
</span><span style="color:#eff1f5;"> </span><span>$</span><span style="color:#bf616a;">message </span><span>= $</span><span style="color:#bf616a;">this</span><span style="color:#eff1f5;">-></span><span style="color:#bf616a;">message</span><span style="color:#eff1f5;">-></span><span style="color:#bf616a;">create</span><span style="color:#eff1f5;">(</span><span style="color:#ebcb8b;">Input</span><span style="color:#eff1f5;">::</span><span style="color:#bf616a;">get</span><span style="color:#eff1f5;">(</span><span>'</span><span style="color:#a3be8c;">message</span><span>'</span><span style="color:#eff1f5;">));
</span><span style="color:#eff1f5;">
</span><span style="color:#eff1f5;"> </span><span>$</span><span style="color:#bf616a;">this</span><span style="color:#eff1f5;">-></span><span style="color:#bf616a;">pusher</span><span style="color:#eff1f5;">-></span><span style="color:#bf616a;">trigger</span><span style="color:#eff1f5;">(</span><span>'</span><span style="color:#a3be8c;">messages</span><span>'</span><span style="color:#eff1f5;">, </span><span>'</span><span style="color:#a3be8c;">new-message</span><span>'</span><span style="color:#eff1f5;">, [</span><span>'</span><span style="color:#a3be8c;">message</span><span>' => $</span><span style="color:#bf616a;">message</span><span style="color:#eff1f5;">-></span><span style="color:#bf616a;">toArray</span><span style="color:#eff1f5;">()]);
</span><span style="color:#eff1f5;">
</span><span style="color:#eff1f5;"> </span><span style="color:#b48ead;">return </span><span style="color:#ebcb8b;">Response</span><span style="color:#eff1f5;">::</span><span style="color:#bf616a;">json</span><span style="color:#eff1f5;">([</span><span>'</span><span style="color:#a3be8c;">message</span><span>' => $</span><span style="color:#bf616a;">message</span><span style="color:#eff1f5;">-></span><span style="color:#bf616a;">toArray</span><span style="color:#eff1f5;">()]);
</span><span style="color:#eff1f5;"> }
</span><span style="color:#eff1f5;">
</span><span style="color:#eff1f5;"> </span><span style="color:#65737e;">/**
</span><span style="color:#65737e;"> * Display the specified resource.
</span><span style="color:#65737e;"> *
</span><span style="color:#65737e;"> * </span><span style="color:#b48ead;">@param</span><span style="color:#65737e;"> int $id
</span><span style="color:#65737e;"> * </span><span style="color:#b48ead;">@return</span><span style="color:#65737e;"> Response
</span><span style="color:#65737e;"> */
</span><span style="color:#eff1f5;"> </span><span style="color:#b48ead;">public function </span><span style="color:#8fa1b3;">show</span><span style="color:#eff1f5;">(</span><span>$</span><span style="color:#bf616a;">id</span><span style="color:#eff1f5;">)
</span><span style="color:#eff1f5;"> {
</span><span style="color:#eff1f5;"> </span><span>$</span><span style="color:#bf616a;">message </span><span>= $</span><span style="color:#bf616a;">this</span><span style="color:#eff1f5;">-></span><span style="color:#bf616a;">message</span><span style="color:#eff1f5;">-></span><span style="color:#bf616a;">findOrFail</span><span style="color:#eff1f5;">(</span><span>$</span><span style="color:#bf616a;">id</span><span style="color:#eff1f5;">);
</span><span style="color:#eff1f5;">
</span><span style="color:#eff1f5;"> </span><span style="color:#b48ead;">return </span><span style="color:#ebcb8b;">Response</span><span style="color:#eff1f5;">::</span><span style="color:#bf616a;">json</span><span style="color:#eff1f5;">([</span><span>'</span><span style="color:#a3be8c;">message</span><span>' => $</span><span style="color:#bf616a;">message</span><span style="color:#eff1f5;">-></span><span style="color:#bf616a;">toArray</span><span style="color:#eff1f5;">()]);
</span><span style="color:#eff1f5;"> }
</span><span style="color:#eff1f5;">}
</span></code></pre>
<p>I have used dependancy injection for the <code>Message</code> model and for <code>PusherManager</code> which are set in the constructor.</p>
<p>You may also notice that the JSON responses wrap <code>message</code> for single objects and <code>messages</code> for collections. This is how Ember will expect its data.</p>
<p>If you're familiar with Ember then you might be already be considering converting the keys to camel case (created_at to createdAt etc). Don't worry about this for now.</p>
<p>You can also see this line of code</p>
<pre style="background-color:#2b303b;color:#c0c5ce;"><code><span>$this->pusher->trigger('messages', 'new-message', ['message' => $message->toArray()]);
</span></code></pre>
<p>When you create a new row in the database you want to tell Pusher so that each client can be notified.</p>
<p>The <code>index</code> method on line 30 takes the latest 5 messages. This as a nice way to introduce newly connected users to the chat without bombarding them with thousands of messages. Feel free to adjust this to your preference.</p>
<p>At this point it's a good idea to test that the API for creating messages works because it's the trickiest to debug.</p>
<p>There are a few ways that you can do this. I've been using <a href="https://luckymarmot.com/paw">Paw</a> which is a really sweet REST client. <a href="https://chrome.google.com/webstore/detail/postman-rest-client/fdmmgilgnpjigdojojpjoooidkmcomcm?hl=en">Postman - REST client</a> is an incredible free alternative. </p>
<p>If you're thinking, "stop throwing all of these stupid apps in my face. Give me the Curl command!" then here you go.</p>
<pre data-lang="bash" style="background-color:#2b303b;color:#c0c5ce;" class="language-bash "><code class="language-bash" data-lang="bash"><span style="color:#bf616a;">curl -X</span><span> POST</span><span style="color:#bf616a;"> -d </span><span>"</span><span style="color:#a3be8c;">message[name]=Mitch</span><span>"</span><span style="color:#bf616a;"> -d </span><span>"</span><span style="color:#a3be8c;">message[body]=this is a message</span><span>" '</span><span style="color:#a3be8c;">http://realtime.dev/messages</span><span>'
</span></code></pre>
<h2 id="ember">Ember</h2>
<p>Here's a preview of the final application:</p>
<p><img src="https://i.imgur.com/03wDiJq.png" alt="Realtime Chat" /></p>
<p>Once the messages reach the input fields a scrollbar will appear. The scrollbar will automatically scroll to the bottom when a new message is received.</p>
<p>Go ahead and install Ember CLI if you haven't already</p>
<pre data-lang="bash" style="background-color:#2b303b;color:#c0c5ce;" class="language-bash "><code class="language-bash" data-lang="bash"><span style="color:#bf616a;">npm</span><span> install</span><span style="color:#bf616a;"> -g</span><span> ember-cli phantomjs
</span></code></pre>
<p>For reference I'm using the following package versions with <a href="https://iojs.org/en/index.html">IO.js v1.2.0</a></p>
<ul>
<li>Ember-CLI 0.2.1</li>
<li>Ember 1.10</li>
<li>Ember-CLI-HTMLBars 0.7.4</li>
<li>Ember Data 1.0.0-beta.16</li>
<li>jQuery 1.11.2</li>
</ul>
<p>Make a new Ember application. I recommend doing so outside of your Laravel directory.</p>
<pre data-lang="bash" style="background-color:#2b303b;color:#c0c5ce;" class="language-bash "><code class="language-bash" data-lang="bash"><span style="color:#bf616a;">ember</span><span> new realtime
</span></code></pre>
<p>After it's set up install the following add ons</p>
<pre data-lang="bash" style="background-color:#2b303b;color:#c0c5ce;" class="language-bash "><code class="language-bash" data-lang="bash"><span style="color:#bf616a;">ember</span><span> install:addon ember-cli-pusher
</span><span style="color:#bf616a;">npm</span><span> install</span><span style="color:#bf616a;"> --save-dev</span><span> ember-cli-content-security-policy
</span></code></pre>
<p>In your configuration file located at <code>config/environment.js</code> add your pusher key to the <code>APP</code> property.</p>
<pre data-lang="js" style="background-color:#2b303b;color:#c0c5ce;" class="language-js "><code class="language-js" data-lang="js"><span>PUSHER_OPTS: {
</span><span> key: '</span><span style="color:#a3be8c;"><KEYHERE></span><span>',
</span><span> connection: {},
</span><span> logAllEvents: </span><span style="color:#d08770;">false
</span><span>}
</span></code></pre>
<p>Then above the <code>return ENV;</code> add these Content Security Policy settings - this is necessary for Pusher's data to be retrieved. You can remove the bootstrap stylesheet in <code>style-src</code> if you wish to include it locally or not at all.</p>
<pre data-lang="js" style="background-color:#2b303b;color:#c0c5ce;" class="language-js "><code class="language-js" data-lang="js"><span style="color:#bf616a;">ENV</span><span>['</span><span style="color:#a3be8c;">contentSecurityPolicy</span><span>'] = {
</span><span> '</span><span style="color:#a3be8c;">default-src</span><span>': "</span><span style="color:#a3be8c;">'none'</span><span>",
</span><span> '</span><span style="color:#a3be8c;">script-src</span><span>': "</span><span style="color:#a3be8c;">'self' http://stats.pusher.com/</span><span>",
</span><span> '</span><span style="color:#a3be8c;">connect-src</span><span>': "</span><span style="color:#a3be8c;">'self' ws://ws.pusherapp.com/</span><span>",
</span><span> '</span><span style="color:#a3be8c;">img-src</span><span>': "</span><span style="color:#a3be8c;">'self'</span><span>",
</span><span> '</span><span style="color:#a3be8c;">style-src</span><span>': "</span><span style="color:#a3be8c;">'self' 'unsafe-inline' http://maxcdn.bootstrapcdn.com/</span><span>",
</span><span> '</span><span style="color:#a3be8c;">media-src</span><span>': "</span><span style="color:#a3be8c;">'self'</span><span>",
</span><span> };
</span></code></pre>
<p>Make <code>messages</code> the root route in <code>app/router.js</code></p>
<pre data-lang="js" style="background-color:#2b303b;color:#c0c5ce;" class="language-js "><code class="language-js" data-lang="js"><span style="color:#b48ead;">import </span><span style="color:#bf616a;">Ember </span><span style="color:#b48ead;">from </span><span>'</span><span style="color:#a3be8c;">ember</span><span>';
</span><span style="color:#b48ead;">import </span><span style="color:#bf616a;">config </span><span style="color:#b48ead;">from </span><span>'</span><span style="color:#a3be8c;">./config/environment</span><span>';
</span><span>
</span><span style="color:#b48ead;">var </span><span style="color:#bf616a;">Router </span><span>= </span><span style="color:#bf616a;">Ember</span><span>.</span><span style="color:#bf616a;">Router</span><span>.</span><span style="color:#8fa1b3;">extend</span><span>({
</span><span> location: </span><span style="color:#bf616a;">config</span><span>.</span><span style="color:#bf616a;">locationType
</span><span>});
</span><span>
</span><span style="color:#bf616a;">Router</span><span>.</span><span style="color:#8fa1b3;">map</span><span>(</span><span style="color:#b48ead;">function</span><span>() {
</span><span> </span><span style="color:#bf616a;">this</span><span>.</span><span style="color:#8fa1b3;">resource</span><span>('</span><span style="color:#a3be8c;">messages</span><span>', {path: '</span><span style="color:#a3be8c;">/</span><span>'}, </span><span style="color:#b48ead;">function</span><span>() {});
</span><span>});
</span><span>
</span><span style="color:#b48ead;">export default </span><span style="color:#bf616a;">Router</span><span>;
</span></code></pre>
<p>Run the following command to change the <code>RestAdapter</code> to one that will work with our API.</p>
<pre style="background-color:#2b303b;color:#c0c5ce;"><code><span>ember g adapter application
</span></code></pre>
<p>In <code>app/adapters/application.js</code> update the adapter name like so:</p>
<pre data-lang="js" style="background-color:#2b303b;color:#c0c5ce;" class="language-js "><code class="language-js" data-lang="js"><span style="color:#b48ead;">import </span><span style="color:#bf616a;">DS </span><span style="color:#b48ead;">from </span><span>'</span><span style="color:#a3be8c;">ember-data</span><span>';
</span><span>
</span><span style="color:#b48ead;">export default </span><span style="color:#bf616a;">DS</span><span>.</span><span style="color:#bf616a;">ActiveModelAdapter</span><span>.</span><span style="color:#8fa1b3;">extend</span><span>({
</span><span>});
</span></code></pre>
<p><code>ActiveModel</code> is a thin wrapper around the Ruby on Rails' ORM <code>ActiveRecord</code>. I'm using this adapter because the API expects data in the same format. This is why you don't need to worry about updating camel case properties to snake case: <code>ActiveModelAdapter</code> handles it all for you.</p>
<p>Create a <code>message</code> model which defines the message properties.</p>
<pre data-lang="bash" style="background-color:#2b303b;color:#c0c5ce;" class="language-bash "><code class="language-bash" data-lang="bash"><span style="color:#bf616a;">ember</span><span> g model message
</span></code></pre>
<pre data-lang="js" style="background-color:#2b303b;color:#c0c5ce;" class="language-js "><code class="language-js" data-lang="js"><span style="color:#65737e;">// app/models/message.js
</span><span style="color:#b48ead;">import </span><span style="color:#bf616a;">DS </span><span style="color:#b48ead;">from </span><span>'</span><span style="color:#a3be8c;">ember-data</span><span>';
</span><span>
</span><span style="color:#b48ead;">export default </span><span style="color:#bf616a;">DS</span><span>.</span><span style="color:#bf616a;">Model</span><span>.</span><span style="color:#8fa1b3;">extend</span><span>({
</span><span> name: </span><span style="color:#bf616a;">DS</span><span>.</span><span style="color:#8fa1b3;">attr</span><span>('</span><span style="color:#a3be8c;">string</span><span>'),
</span><span> body: </span><span style="color:#bf616a;">DS</span><span>.</span><span style="color:#8fa1b3;">attr</span><span>('</span><span style="color:#a3be8c;">string</span><span>'),
</span><span> createdAt: </span><span style="color:#bf616a;">DS</span><span>.</span><span style="color:#8fa1b3;">attr</span><span>('</span><span style="color:#a3be8c;">string</span><span>'),
</span><span> updatedAt: </span><span style="color:#bf616a;">DS</span><span>.</span><span style="color:#8fa1b3;">attr</span><span>('</span><span style="color:#a3be8c;">string</span><span>')
</span><span>});
</span></code></pre>
<p>Now create the messages route which will use the message model</p>
<pre data-lang="bash" style="background-color:#2b303b;color:#c0c5ce;" class="language-bash "><code class="language-bash" data-lang="bash"><span style="color:#bf616a;">ember</span><span> g route messages
</span></code></pre>
<pre data-lang="js" style="background-color:#2b303b;color:#c0c5ce;" class="language-js "><code class="language-js" data-lang="js"><span style="color:#65737e;">// app/routes/messages.js
</span><span style="color:#b48ead;">import </span><span style="color:#bf616a;">Ember </span><span style="color:#b48ead;">from </span><span>'</span><span style="color:#a3be8c;">ember</span><span>';
</span><span>
</span><span style="color:#b48ead;">export default </span><span style="color:#bf616a;">Ember</span><span>.</span><span style="color:#bf616a;">Route</span><span>.</span><span style="color:#8fa1b3;">extend</span><span>({
</span><span> </span><span style="color:#8fa1b3;">model</span><span>: </span><span style="color:#b48ead;">function</span><span>() {
</span><span> </span><span style="color:#b48ead;">return </span><span style="color:#bf616a;">this</span><span>.</span><span style="color:#bf616a;">store</span><span>.</span><span style="color:#96b5b4;">find</span><span>('</span><span style="color:#a3be8c;">message</span><span>');
</span><span> }
</span><span>});
</span></code></pre>
<p>The messages controller is the most complex part of the application. Create it with the following command.</p>
<pre data-lang="bash" style="background-color:#2b303b;color:#c0c5ce;" class="language-bash "><code class="language-bash" data-lang="bash"><span style="color:#bf616a;">ember</span><span> g controller messages
</span></code></pre>
<pre data-lang="js" style="background-color:#2b303b;color:#c0c5ce;" class="language-js "><code class="language-js" data-lang="js"><span style="color:#65737e;">// app/controllers/messages.js
</span><span style="color:#b48ead;">import </span><span style="color:#bf616a;">Ember </span><span style="color:#b48ead;">from </span><span>'</span><span style="color:#a3be8c;">ember</span><span>';
</span><span style="color:#b48ead;">import </span><span>{ </span><span style="color:#bf616a;">Bindings </span><span>} </span><span style="color:#b48ead;">from </span><span>'</span><span style="color:#a3be8c;">ember-pusher/bindings</span><span>';
</span><span>
</span><span style="color:#b48ead;">export default </span><span style="color:#bf616a;">Ember</span><span>.</span><span style="color:#bf616a;">ArrayController</span><span>.</span><span style="color:#8fa1b3;">extend</span><span>(</span><span style="color:#bf616a;">Bindings</span><span>, {
</span><span> sortProperties: ['</span><span style="color:#a3be8c;">createdAt</span><span>'],
</span><span> sortAscending: </span><span style="color:#d08770;">true</span><span>,
</span><span> logPusherEvents: </span><span style="color:#d08770;">true</span><span>,
</span><span> PUSHER_SUBSCRIPTIONS: {
</span><span> messages: ['</span><span style="color:#a3be8c;">new-message</span><span>']
</span><span> },
</span><span> </span><span style="color:#8fa1b3;">isValid</span><span>: </span><span style="color:#b48ead;">function</span><span>() {
</span><span> </span><span style="color:#b48ead;">return </span><span>(</span><span style="color:#bf616a;">this</span><span>.</span><span style="color:#96b5b4;">get</span><span>('</span><span style="color:#a3be8c;">name</span><span>') && </span><span style="color:#bf616a;">this</span><span>.</span><span style="color:#96b5b4;">get</span><span>('</span><span style="color:#a3be8c;">body</span><span>'));
</span><span> }.</span><span style="color:#8fa1b3;">property</span><span>('</span><span style="color:#a3be8c;">name</span><span>', '</span><span style="color:#a3be8c;">body</span><span>'),
</span><span> actions: {
</span><span> </span><span style="color:#8fa1b3;">newMessage</span><span>: </span><span style="color:#b48ead;">function</span><span>(</span><span style="color:#bf616a;">data</span><span>) {
</span><span> </span><span style="color:#b48ead;">var </span><span style="color:#bf616a;">_this </span><span>= </span><span style="color:#bf616a;">this</span><span>,
</span><span> </span><span style="color:#bf616a;">chat </span><span>= </span><span style="color:#8fa1b3;">jQuery</span><span>('</span><span style="color:#a3be8c;">#chat</span><span>');
</span><span>
</span><span> </span><span style="color:#bf616a;">Ember</span><span>.</span><span style="color:#bf616a;">run</span><span>.</span><span style="color:#8fa1b3;">later</span><span>((</span><span style="color:#b48ead;">function</span><span>() {
</span><span> </span><span style="color:#bf616a;">_this</span><span>.</span><span style="color:#bf616a;">store</span><span>.</span><span style="color:#96b5b4;">push</span><span>('</span><span style="color:#a3be8c;">message</span><span>', </span><span style="color:#bf616a;">_this</span><span>.</span><span style="color:#bf616a;">store</span><span>.</span><span style="color:#96b5b4;">normalize</span><span>('</span><span style="color:#a3be8c;">message</span><span>', </span><span style="color:#bf616a;">data</span><span>.</span><span style="color:#bf616a;">message</span><span>));
</span><span> </span><span style="color:#bf616a;">Ember</span><span>.</span><span style="color:#bf616a;">run</span><span>.</span><span style="color:#8fa1b3;">schedule</span><span>( '</span><span style="color:#a3be8c;">afterRender</span><span>', </span><span style="color:#b48ead;">function </span><span>() {
</span><span> </span><span style="color:#bf616a;">chat</span><span>.</span><span style="color:#96b5b4;">animate</span><span>({scrollTop: </span><span style="color:#bf616a;">chat</span><span>[</span><span style="color:#d08770;">0</span><span>].</span><span style="color:#bf616a;">scrollHeight </span><span>});
</span><span> });
</span><span> }), </span><span style="color:#d08770;">200</span><span>);
</span><span> },
</span><span> </span><span style="color:#8fa1b3;">send</span><span>: </span><span style="color:#b48ead;">function</span><span>() {
</span><span> </span><span style="color:#b48ead;">var </span><span style="color:#bf616a;">_this </span><span>= </span><span style="color:#bf616a;">this</span><span>;
</span><span> </span><span style="color:#b48ead;">if</span><span>(</span><span style="color:#bf616a;">this</span><span>.</span><span style="color:#96b5b4;">get</span><span>('</span><span style="color:#a3be8c;">isValid</span><span>')) {
</span><span> </span><span style="color:#b48ead;">var </span><span style="color:#bf616a;">message </span><span>= </span><span style="color:#bf616a;">this</span><span>.</span><span style="color:#bf616a;">store</span><span>.</span><span style="color:#8fa1b3;">createRecord</span><span>('</span><span style="color:#a3be8c;">message</span><span>', {
</span><span> name: </span><span style="color:#bf616a;">this</span><span>.name,
</span><span> body: </span><span style="color:#bf616a;">this</span><span>.body
</span><span> });
</span><span> </span><span style="color:#bf616a;">message</span><span>.</span><span style="color:#8fa1b3;">save</span><span>().</span><span style="color:#96b5b4;">then</span><span>(</span><span style="color:#b48ead;">function</span><span>() {
</span><span> </span><span style="color:#bf616a;">_this</span><span>.</span><span style="color:#96b5b4;">set</span><span>('</span><span style="color:#a3be8c;">body</span><span>', '');
</span><span> });
</span><span> } </span><span style="color:#b48ead;">else </span><span>{
</span><span> </span><span style="color:#8fa1b3;">alert</span><span>('</span><span style="color:#a3be8c;">Please enter a name and a message.</span><span>');
</span><span> }
</span><span> }
</span><span> }
</span><span>});
</span></code></pre>
<p>There's a lot of interesting stuff going on here.</p>
<p>You're importing Ember-pusher on line 2 and on lines 8-10 we subscribe to the 'new-message' event from Pusher. When you receive this event Ember will automatically call the action of the same name (but camel cased).</p>
<p>On lines 6 and 7 you're setting how the data will be sorted. Chat systems generally show the latest messages at the bottom so that's why <code>createdAt</code> is sorted ascending. Do not sort by id because <code>sortProperties</code> only supports strings and therefore '10' comes before '2'. This will give the appearance of a completely random order.</p>
<p>This <code>newMessage</code> action pushes the new message onto the model. It waits a short moment to prevent duplicate entries on the client of the original message with <code>Ember.run.later</code>.</p>
<p>The scrollbar for the chat system needs to automatically scroll to the bottom so that new messages can be seen. To do this <code>Ember.run.schedule</code> is run with <code>afterRender</code> (after the new message has been added to the DOM) and then jQuery is used to animate the chat to the bottom of the screen.</p>
<p>The last action, <code>send</code>, is called when the form in the template is submitted. The <code>isValid</code> property checks that the <code>name</code> and <code>body</code> has been filled in before sending to the API. Then after the message is saved the <code>body</code> value is cleared so the user can start writing new messages in the input field.</p>
<p>For the user interface I've chosen to use Twitter Bootstrap 3.2.</p>
<p>Add the bootstrap stylesheet it into <code>app/index.html</code></p>
<pre data-lang="html" style="background-color:#2b303b;color:#c0c5ce;" class="language-html "><code class="language-html" data-lang="html"><span><</span><span style="color:#bf616a;">link </span><span style="color:#d08770;">rel</span><span>="</span><span style="color:#a3be8c;">stylesheet</span><span>" </span><span style="color:#d08770;">href</span><span>="</span><span style="color:#a3be8c;">//maxcdn.bootstrapcdn.com/bootstrap/3.3.4/css/bootstrap.min.css</span><span>">
</span></code></pre>
<p>and add the following styles into <code>app/styles/app.css</code></p>
<pre data-lang="css" style="background-color:#2b303b;color:#c0c5ce;" class="language-css "><code class="language-css" data-lang="css"><span style="color:#8fa1b3;">.</span><span style="color:#d08770;">message </span><span>{
</span><span> background-color: </span><span style="color:#96b5b4;">#f3f3f4</span><span>;
</span><span> color: </span><span style="color:#96b5b4;">rgba</span><span>(</span><span style="color:#d08770;">0</span><span>, </span><span style="color:#d08770;">0</span><span>, </span><span style="color:#d08770;">0</span><span>, </span><span style="color:#d08770;">0.5</span><span>);
</span><span> border-color: </span><span style="color:#96b5b4;">rgba</span><span>(</span><span style="color:#d08770;">0</span><span>, </span><span style="color:#d08770;">0</span><span>, </span><span style="color:#d08770;">0</span><span>, </span><span style="color:#d08770;">0.1</span><span>);
</span><span> margin-top: </span><span style="color:#d08770;">20px</span><span>;
</span><span> margin-bottom: </span><span style="color:#d08770;">0px</span><span>;
</span><span> padding: </span><span style="color:#d08770;">15px</span><span>;
</span><span> border: </span><span style="color:#d08770;">1px </span><span>solid;
</span><span> border-radius: </span><span style="color:#d08770;">4px</span><span>;
</span><span>}
</span><span style="color:#8fa1b3;">.</span><span style="color:#d08770;">message</span><span style="color:#8fa1b3;">:</span><span style="color:#b48ead;">first-child </span><span>{
</span><span> margin-top: </span><span style="color:#d08770;">0px</span><span>;
</span><span>}
</span><span>
</span><span style="color:#8fa1b3;">#chat </span><span>{
</span><span> overflow: auto;
</span><span> height: </span><span style="color:#d08770;">340px</span><span>;
</span><span>}
</span><span>
</span><span style="color:#8fa1b3;">.</span><span style="color:#d08770;">form-chat </span><span>{
</span><span> margin-top: </span><span style="color:#d08770;">20px</span><span>;
</span><span>}
</span></code></pre>
<p>Update <code>app/templates/application.hbs</code> like so</p>
<pre data-lang="html" style="background-color:#2b303b;color:#c0c5ce;" class="language-html "><code class="language-html" data-lang="html"><span><</span><span style="color:#bf616a;">div </span><span style="color:#d08770;">class</span><span>="</span><span style="color:#a3be8c;">container</span><span>">
</span><span> <</span><span style="color:#bf616a;">div </span><span style="color:#d08770;">class</span><span>="</span><span style="color:#a3be8c;">row</span><span>">
</span><span> <</span><span style="color:#bf616a;">div </span><span style="color:#d08770;">class</span><span>="</span><span style="color:#a3be8c;">col-xs-12</span><span>">
</span><span> <</span><span style="color:#bf616a;">h2 </span><span style="color:#8fa1b3;">id</span><span>="</span><span style="color:#a3be8c;">title</span><span>">Ember Chat</</span><span style="color:#bf616a;">h2</span><span>>
</span><span> </</span><span style="color:#bf616a;">div</span><span>>
</span><span> </</span><span style="color:#bf616a;">div</span><span>>
</span><span> {{outlet}}
</span><span></</span><span style="color:#bf616a;">div</span><span>>
</span></code></pre>
<p>Then in <code>app/templates/messages.hbs</code></p>
<pre data-lang="html" style="background-color:#2b303b;color:#c0c5ce;" class="language-html "><code class="language-html" data-lang="html"><span> <</span><span style="color:#bf616a;">div </span><span style="color:#d08770;">class</span><span>="</span><span style="color:#a3be8c;">row</span><span>">
</span><span> <</span><span style="color:#bf616a;">div </span><span style="color:#d08770;">class</span><span>="</span><span style="color:#a3be8c;">col-xs-12</span><span>">
</span><span> <</span><span style="color:#bf616a;">div </span><span style="color:#8fa1b3;">id</span><span>="</span><span style="color:#a3be8c;">chat</span><span>">
</span><span> {{#each message in arrangedContent}}
</span><span> {{message-row message=message}}
</span><span> {{/each}}
</span><span> </</span><span style="color:#bf616a;">div</span><span>>
</span><span> </</span><span style="color:#bf616a;">div</span><span>>
</span><span> </</span><span style="color:#bf616a;">div</span><span>>
</span><span> <</span><span style="color:#bf616a;">form </span><span style="color:#d08770;">{{action </span><span style="background-color:#bf616a;color:#2b303b;">"</span><span style="color:#d08770;">send</span><span style="background-color:#bf616a;color:#2b303b;">"</span><span> </span><span style="color:#d08770;">on</span><span>="</span><span style="color:#a3be8c;">submit</span><span>"</span><span style="color:#d08770;">}} class</span><span>="</span><span style="color:#a3be8c;">form-chat</span><span>">
</span><span> <</span><span style="color:#bf616a;">div </span><span style="color:#d08770;">class</span><span>="</span><span style="color:#a3be8c;">row</span><span>">
</span><span> <</span><span style="color:#bf616a;">div </span><span style="color:#d08770;">class</span><span>="</span><span style="color:#a3be8c;">col-xs-2</span><span>">
</span><span> {{input placeholder="Name" classNames="form-control" value=name}}
</span><span> </</span><span style="color:#bf616a;">div</span><span>>
</span><span> <</span><span style="color:#bf616a;">div </span><span style="color:#d08770;">class</span><span>="</span><span style="color:#a3be8c;">col-xs-9</span><span>">
</span><span> {{input placeholder="Message" classNames="form-control" value=body}}
</span><span> </</span><span style="color:#bf616a;">div</span><span>>
</span><span> <</span><span style="color:#bf616a;">div </span><span style="color:#d08770;">class</span><span>="</span><span style="color:#a3be8c;">col-xs-1 text-right</span><span>">
</span><span> <</span><span style="color:#bf616a;">button </span><span style="color:#d08770;">{{bind-attr class</span><span>="</span><span style="color:#a3be8c;">:btn :btn-primary isValid::disabled</span><span>"</span><span style="color:#d08770;">}}</span><span>>Send</</span><span style="color:#bf616a;">button</span><span>>
</span><span> </</span><span style="color:#bf616a;">div</span><span>>
</span><span> </</span><span style="color:#bf616a;">div</span><span>>
</span><span> </</span><span style="color:#bf616a;">form</span><span>>
</span></code></pre>
<p>Here there is a form with an action <code>send</code> which is sent to the messages controller. The send button has 2 static classes and 1 dynamic one. When the controller's <code>isValid</code> property is false then the <code>disabled</code> class will be added to the button - giving it the appearance of being unusable.</p>
<p>On lines 4-6 the messages are looped through and sent to a <code>message-row</code> component which we'll create next.</p>
<pre style="background-color:#2b303b;color:#c0c5ce;"><code><span>ember g component message-row
</span></code></pre>
<p>in <code>app/components/message-row.js</code></p>
<pre data-lang="js" style="background-color:#2b303b;color:#c0c5ce;" class="language-js "><code class="language-js" data-lang="js"><span style="color:#b48ead;">import </span><span style="color:#bf616a;">Ember </span><span style="color:#b48ead;">from </span><span>'</span><span style="color:#a3be8c;">ember</span><span>';
</span><span>
</span><span style="color:#b48ead;">export default </span><span style="color:#bf616a;">Ember</span><span>.</span><span style="color:#bf616a;">Component</span><span>.</span><span style="color:#8fa1b3;">extend</span><span>({
</span><span> tagName: '</span><span style="color:#a3be8c;">div</span><span>',
</span><span> classNames: ['</span><span style="color:#a3be8c;">message</span><span>'],
</span><span> message: </span><span style="color:#d08770;">null
</span><span>});
</span></code></pre>
<p>And for the template in <code>app/templates/components/message-row.hbs</code></p>
<pre data-lang="html" style="background-color:#2b303b;color:#c0c5ce;" class="language-html "><code class="language-html" data-lang="html"><span>[{{message.createdAt}}] {{message.name}}: {{message.body}}
</span></code></pre>
<p>Components are partials with no context. You can adjust their settings in <code>app/components/*.js</code> and adjust the templates in <code>app/templates/components/*.hbs</code>.</p>
<p>If you haven't already then fire up Ember! Note the proxy argument which allows you to use Ember with your Laravel API.</p>
<pre style="background-color:#2b303b;color:#c0c5ce;"><code><span>ember server --proxy http://realtime.dev
</span></code></pre>
<h2 id="preview">Preview</h2>
<p>Here's a short video of the system in action. There are two browser windows are used to show the new messages being loaded in realtime.</p>
<div style='position:relative; padding-bottom:56.25%'>
<iframe src="http://gfycat.com/ifr/RichTinyDegus" frameborder="0" scrolling="no" width="100%" height="100%" style='position:absolute;top:0;left:0;' ></iframe>
</div>
<h2 id="conclusion">Conclusion</h2>
<p>In this article I have shown you how to set up a simple Laravel 5 API that's compatible with Ember JS. I used Pusher to send realtime notifications to clients and discussed some of the alternatives briefly. I then set up the chat system in Ember which sends and receives messages in realtime.</p>
<p>In some ways I've found this project easy but in others I feel I have failed. I'm quite disapointed that I couldn't get Firehose to work with Ember and if I had more time I would have liked to have pursued it further.</p>
<p>This application was done in one weekend and in my opinion it is far from finished. It could definitely do with authentication and there are some kinks that need to be ironed out.</p>
<p>One kink in particular is when a client sends a message it's pushed to the top of the chat because the <code>createdAt</code> timestamp is not set. This fixes itself when the Pusher event updates it but it's not ideal.</p>
<p>I believe this could easily be fixed by adding <code>moment.js</code> and inserting the current time when creating a new record. If you've created the application yourself then I definitely recommend doing this as the next step.</p>
Using Padrino with Ember CLI Part 5: Realtime2015-03-12T00:00:00+00:002015-03-12T00:00:00+00:00https://www.fullstackstanley.com/articles/using-padrino-with-ember-cli-part-5-realtime/<p><em><strong>Note</strong>: This article was updated on 8th April 2015 thanks to <a href="https://twitter.com/leggetter">Phil</a> from <a href="https://twitter.com/pusher">Pusher</a> with a better solution for preventing duplicate data. You can see the content of the original post <a href="https://github.com/acoustep/mitchs.pw/blob/68f30561cd725758eccd103b3de98eb24f982bf9/source/articles/2015-03-12-using-padrino-with-ember-cli-part-5-realtime.html.markdown">here</a>.</em></p>
<span id="continue-reading"></span>
<p>In the final installment of my Padrino + Ember series I'd like to show you how to get some basic realtime functionality within our application. We'll be using <a href="http://pusher.com">Pusher</a> to send and receive messages. They have a pretty reasonable free package which includes 100k messages per day, 20 max connections and SSL if you need it.</p>
<p><a href="https://github.com/acoustep/padrino-ember-example">You can view the full source for the application on Github</a></p>
<p>Make sure you've signed up at <a href="http://pusher.com">pusher.com</a>, make an application and make a note of your app_id, key and secret key.</p>
<h2 id="padrino">Padrino</h2>
<p>Add the Pusher gem to your <code>Gemfile</code> and run <code>bundle install</code>.</p>
<pre data-lang="ruby" style="background-color:#2b303b;color:#c0c5ce;" class="language-ruby "><code class="language-ruby" data-lang="ruby"><span style="color:#8fa1b3;">gem </span><span>'</span><span style="color:#a3be8c;">pusher</span><span>'
</span></code></pre>
<p>Inside your App class in <code>app/app.rb</code> add the following configuration block with your Pusher credentials. For production I do recommend storing these with <a href="https://github.com/bkeepers/dotenv">dotenv</a> but I'll leave that for you to decide.</p>
<pre data-lang="ruby" style="background-color:#2b303b;color:#c0c5ce;" class="language-ruby "><code class="language-ruby" data-lang="ruby"><span> configure </span><span style="color:#b48ead;">do
</span><span> </span><span style="color:#ebcb8b;">Pusher</span><span>.app_id = ''
</span><span> </span><span style="color:#ebcb8b;">Pusher</span><span>.key = ''
</span><span> </span><span style="color:#ebcb8b;">Pusher</span><span>.secret = ''
</span><span> </span><span style="color:#b48ead;">end
</span></code></pre>
<p>Here are the changes to the posts controller</p>
<pre data-lang="ruby" style="background-color:#2b303b;color:#c0c5ce;" class="language-ruby "><code class="language-ruby" data-lang="ruby"><span style="color:#65737e;"># app/controllers/posts.rb
</span><span> </span><span style="color:#65737e;"># ...
</span><span> post </span><span style="color:#a3be8c;">:create</span><span>, </span><span style="color:#a3be8c;">map: </span><span>"" </span><span style="color:#b48ead;">do
</span><span> parameters = post_params
</span><span> </span><span style="color:#b48ead;">if</span><span> parameters["</span><span style="color:#a3be8c;">post</span><span>"].</span><span style="color:#96b5b4;">nil?
</span><span> </span><span style="color:#b48ead;">return </span><span>'</span><span style="color:#a3be8c;">{}</span><span>'
</span><span> </span><span style="color:#b48ead;">end
</span><span> @</span><span style="color:#bf616a;">post </span><span>= </span><span style="color:#ebcb8b;">Post</span><span>.create parameters["</span><span style="color:#a3be8c;">post</span><span>"].except("</span><span style="color:#a3be8c;">socket_id</span><span>")
</span><span>
</span><span> </span><span style="color:#ebcb8b;">Pusher</span><span>['</span><span style="color:#a3be8c;">posts</span><span>'].trigger('</span><span style="color:#a3be8c;">new-post</span><span>', {</span><span style="color:#a3be8c;">post: </span><span>@</span><span style="color:#bf616a;">post</span><span>.values}, parameters["</span><span style="color:#a3be8c;">post</span><span>"]["</span><span style="color:#a3be8c;">socket_id</span><span>"])
</span><span>
</span><span> render "</span><span style="color:#a3be8c;">posts/show</span><span>"
</span><span> </span><span style="color:#b48ead;">end
</span><span>
</span><span> </span><span style="color:#65737e;"># ...
</span><span>
</span><span> put </span><span style="color:#a3be8c;">:update</span><span>, </span><span style="color:#a3be8c;">map: </span><span>"</span><span style="color:#a3be8c;">:id</span><span>" </span><span style="color:#b48ead;">do
</span><span> @</span><span style="color:#bf616a;">post </span><span>= </span><span style="color:#ebcb8b;">Post</span><span>[params[</span><span style="color:#a3be8c;">:id</span><span>]]
</span><span>
</span><span> </span><span style="color:#b48ead;">if </span><span>@</span><span style="color:#bf616a;">post</span><span>.</span><span style="color:#96b5b4;">nil?
</span><span> </span><span style="color:#b48ead;">return </span><span>'</span><span style="color:#a3be8c;">{}</span><span>'
</span><span> </span><span style="color:#b48ead;">end
</span><span>
</span><span> parameters = post_params
</span><span> @</span><span style="color:#bf616a;">post</span><span>.update parameters["</span><span style="color:#a3be8c;">post</span><span>"].except("</span><span style="color:#a3be8c;">socket_id</span><span>")
</span><span> render "</span><span style="color:#a3be8c;">posts/show</span><span>"
</span><span>
</span><span> </span><span style="color:#b48ead;">end
</span><span> </span><span style="color:#65737e;"># ...
</span></code></pre>
<p>We're going to be sending a new parameter called <code>socket_id</code> to our Padrino API from the Ember application. <code>socket_id</code> is generated from Pusher in the Ember application and is unique for each user.</p>
<p>Notice on lines 8 and 25 we exclude <code>socket_id</code> as we don't have a <code>socket_id</code> field in our posts table to insert.</p>
<p>On line 10 we send the data to Pusher to tell applications that there is a new blog post. The key, <code>posts</code>, is the channel. The first parameter <code>new-post</code> is the event, the second parameter is the data to send and the third parameter is client's socket id.</p>
<p>Sending the client's socket id means that Pusher will not send the event back to the user which created it. This will prevent duplicate data from showing in the poster's Ember application.</p>
<p>That's it for Padrino. It's really that simple!</p>
<p>Before moving onto the Ember part of the application make sure that Pusher is receiving the messages. The easiest way to do this is to create a post in the Ember app and then go to the debug log in the Pusher dashboard.</p>
<p><img src="https://i.imgur.com/gc6N497.png" alt="Pusher debugging" /></p>
<h2 id="ember">Ember</h2>
<p>We need to install 2 addons for Ember. One of which is for Pusher and can be installed via command line</p>
<p><code>ember install:addon ember-cli-pusher</code></p>
<p>The other is to deal with CSP (Content Security Policy) when using Pusher.</p>
<p>Without it we will get an error from our application like the following</p>
<p><code>[Report Only] Refused to load the script 'http://stats.pusher.com/timeline/v2/jsonp/1?session=...' because it violates the following Content Security Policy directive: "script-src 'self' 'unsafe-eval' localhost:35729 0.0.0.0:35729".</code></p>
<p>To fix this we use <code>ember-cli-content-security-policy</code>.</p>
<p>Let's add it to our <code>package.json</code>. Currently we can't install it via command line because there's a bug in the latest tagged release. We need to get the latest version from Github.</p>
<pre data-lang="js" style="background-color:#2b303b;color:#c0c5ce;" class="language-js "><code class="language-js" data-lang="js"><span>"</span><span style="color:#a3be8c;">ember-cli-content-security-policy</span><span>": "</span><span style="color:#a3be8c;">git://github.com/rwjblue/ember-cli-content-security-policy.git#master</span><span>",
</span></code></pre>
<p>Run <code>npm install</code> afterwards.</p>
<p>Next we'll update <code>config/environment.js</code>. Make sure you put your Pusher key in the Pusher settings.</p>
<pre data-lang="js" style="background-color:#2b303b;color:#c0c5ce;" class="language-js "><code class="language-js" data-lang="js"><span> APP: {
</span><span> </span><span style="color:#65737e;">// Here you can pass flags/options to your application instance
</span><span> </span><span style="color:#65737e;">// when it is created
</span><span> PUSHER_OPTS: {
</span><span> key: '</span><span style="color:#a3be8c;"><YOURKEY></span><span>',
</span><span> connection: {},
</span><span> logAllEvents: </span><span style="color:#d08770;">false
</span><span> }
</span><span> },
</span></code></pre>
<p>We can add these CSP settings with the other ENV related content. Note that Bootstrap CDN is also included for fonts and stylesheets.</p>
<pre data-lang="js" style="background-color:#2b303b;color:#c0c5ce;" class="language-js "><code class="language-js" data-lang="js"><span> </span><span style="color:#bf616a;">ENV</span><span>['</span><span style="color:#a3be8c;">contentSecurityPolicy</span><span>'] = {
</span><span> '</span><span style="color:#a3be8c;">default-src</span><span>': "</span><span style="color:#a3be8c;">'none'</span><span>",
</span><span> '</span><span style="color:#a3be8c;">font-src</span><span>': "</span><span style="color:#a3be8c;">'self' http://maxcdn.bootstrapcdn.com/</span><span>",
</span><span> '</span><span style="color:#a3be8c;">script-src</span><span>': "</span><span style="color:#a3be8c;">'self' http://stats.pusher.com/</span><span>",
</span><span> '</span><span style="color:#a3be8c;">connect-src</span><span>': "</span><span style="color:#a3be8c;">'self' ws://ws.pusherapp.com/</span><span>",
</span><span> '</span><span style="color:#a3be8c;">img-src</span><span>': "</span><span style="color:#a3be8c;">'self'</span><span>",
</span><span> '</span><span style="color:#a3be8c;">style-src</span><span>': "</span><span style="color:#a3be8c;">'self' 'unsafe-inline' http://maxcdn.bootstrapcdn.com/</span><span>",
</span><span> '</span><span style="color:#a3be8c;">media-src</span><span>': "</span><span style="color:#a3be8c;">'self'</span><span>",
</span><span> };
</span></code></pre>
<p>Now that we have Pusher set up we need to generate the controller for the posts/index and modify it to receive the events.</p>
<pre style="background-color:#2b303b;color:#c0c5ce;"><code><span>ember g controller posts/index
</span></code></pre>
<p>In <code>app/controllers/posts/index.js</code> add the following:</p>
<pre data-lang="js" style="background-color:#2b303b;color:#c0c5ce;" class="language-js "><code class="language-js" data-lang="js"><span style="color:#b48ead;">import </span><span style="color:#bf616a;">Ember </span><span style="color:#b48ead;">from </span><span>'</span><span style="color:#a3be8c;">ember</span><span>';
</span><span style="color:#b48ead;">import </span><span>{ </span><span style="color:#bf616a;">Bindings </span><span>} </span><span style="color:#b48ead;">from </span><span>'</span><span style="color:#a3be8c;">ember-pusher/bindings</span><span>';
</span><span>
</span><span style="color:#b48ead;">export default </span><span style="color:#bf616a;">Ember</span><span>.</span><span style="color:#bf616a;">ArrayController</span><span>.</span><span style="color:#8fa1b3;">extend</span><span>(</span><span style="color:#bf616a;">Bindings</span><span>, {
</span><span> sortProperties: ['</span><span style="color:#a3be8c;">id</span><span>'],
</span><span> sortAscending: </span><span style="color:#d08770;">false</span><span>,
</span><span> logPusherEvents: </span><span style="color:#d08770;">true</span><span>,
</span><span> PUSHER_SUBSCRIPTIONS: {
</span><span> posts: ['</span><span style="color:#a3be8c;">new-post</span><span>']
</span><span> },
</span><span> actions: {
</span><span> </span><span style="color:#8fa1b3;">newPost</span><span>: </span><span style="color:#b48ead;">function</span><span>(</span><span style="color:#bf616a;">message</span><span>) {
</span><span> </span><span style="color:#bf616a;">this</span><span>.</span><span style="color:#bf616a;">store</span><span>.</span><span style="color:#96b5b4;">push</span><span>('</span><span style="color:#a3be8c;">post</span><span>', </span><span style="color:#bf616a;">this</span><span>.</span><span style="color:#bf616a;">store</span><span>.</span><span style="color:#96b5b4;">normalize</span><span>('</span><span style="color:#a3be8c;">post</span><span>', </span><span style="color:#bf616a;">message</span><span>.</span><span style="color:#bf616a;">post</span><span>));
</span><span> }
</span><span> }
</span><span>});
</span></code></pre>
<p>We will switch the controller to an ArrayController so we can sort by <code>id</code> descending (so that newer posts appear at the top). Using the pusher library we subscribe to the posts channel and the <code>new-post</code> event. Note that when there is a <code>new-post</code> event an action is called with the camel-case version of the name. In this case <code>new-post</code> becomes <code>newPost</code>.</p>
<p>Inside of the <code>newPost</code> action we use <code>store.push</code> which will find the post if it's already there or create it otherwise.</p>
<p>Here is the newly updated <code>posts/index.hbs</code></p>
<pre data-lang="html" style="background-color:#2b303b;color:#c0c5ce;" class="language-html "><code class="language-html" data-lang="html"><span><</span><span style="color:#bf616a;">h3</span><span>>Latest Posts</</span><span style="color:#bf616a;">h3</span><span>>
</span><span><</span><span style="color:#bf616a;">table </span><span style="color:#d08770;">class</span><span>="</span><span style="color:#a3be8c;">table table-striped</span><span>">
</span><span> <</span><span style="color:#bf616a;">tr</span><span>>
</span><span> <</span><span style="color:#bf616a;">th </span><span style="color:#d08770;">class</span><span>=</span><span style="color:#a3be8c;">“col-md-3”</span><span>>Title</</span><span style="color:#bf616a;">th</span><span>>
</span><span> <</span><span style="color:#bf616a;">th </span><span style="color:#d08770;">class</span><span>="</span><span style="color:#a3be8c;">col-md-3</span><span>">Created at</</span><span style="color:#bf616a;">th</span><span>>
</span><span> <</span><span style="color:#bf616a;">th </span><span style="color:#d08770;">class</span><span>="</span><span style="color:#a3be8c;">col-md-3</span><span>">Last modified</</span><span style="color:#bf616a;">th</span><span>>
</span><span> <</span><span style="color:#bf616a;">th </span><span style="color:#d08770;">class</span><span>="</span><span style="color:#a3be8c;">col-md-3</span><span>">Actions</</span><span style="color:#bf616a;">th</span><span>>
</span><span> </</span><span style="color:#bf616a;">tr</span><span>>
</span><span>{{#each post in arrangedContent}}
</span><span><</span><span style="color:#bf616a;">tr</span><span>>
</span><span> <</span><span style="color:#bf616a;">td</span><span>>{{ post.title }}</</span><span style="color:#bf616a;">td</span><span>>
</span><span> <</span><span style="color:#bf616a;">td</span><span>>{{ post.createdAt }}</</span><span style="color:#bf616a;">td</span><span>>
</span><span> <</span><span style="color:#bf616a;">td</span><span>>{{ post.updatedAt }}</</span><span style="color:#bf616a;">td</span><span>>
</span><span> <</span><span style="color:#bf616a;">td</span><span>>
</span><span> {{#link-to 'posts.show' post classNames="btn btn-success"}}View{{/link-to}}
</span><span> {{#if session.isAuthenticated}}
</span><span> {{#link-to 'posts.edit' post classNames="btn btn-info"}}Edit{{/link-to}}
</span><span> <</span><span style="color:#bf616a;">button </span><span style="color:#d08770;">{{action </span><span style="background-color:#bf616a;color:#2b303b;">'</span><span style="color:#d08770;">delete</span><span style="background-color:#bf616a;color:#2b303b;">'</span><span> </span><span style="color:#d08770;">post}} class</span><span>="</span><span style="color:#a3be8c;">btn btn-danger</span><span>">Delete</</span><span style="color:#bf616a;">button</span><span>>
</span><span> {{/if}}
</span><span> </</span><span style="color:#bf616a;">td</span><span>>
</span><span></</span><span style="color:#bf616a;">tr</span><span>>
</span><span>{{/each}}
</span><span></</span><span style="color:#bf616a;">table</span><span>>
</span><span>{{#if session.isAuthenticated}}
</span><span> {{#link-to 'posts.new' classNames="btn btn-primary"}}
</span><span> New Post
</span><span> {{/link-to}}
</span><span>{{/if}}
</span></code></pre>
<p>Note that we're looping through <code>arrangedContent</code> now so that we that the data is ordered by ID descending.</p>
<p>At this point posts will get pushed to the on top until we hit an id of 10 which mysteriously goes to the bottom of the collection. This is because Ember's sort works with strings not integers. The simplest solution is to switch <code>id</code> out for <code>createdAt</code> in your <code>posts/index</code> controller.</p>
<pre data-lang="js" style="background-color:#2b303b;color:#c0c5ce;" class="language-js "><code class="language-js" data-lang="js"><span>sortProperties: ['</span><span style="color:#a3be8c;">createdAt</span><span>'],
</span></code></pre>
<p>As mentioned in the Padrino part of the article we need to add a new parameter, <code>socketId</code> to the <code>post</code> model.</p>
<pre data-lang="js" style="background-color:#2b303b;color:#c0c5ce;" class="language-js "><code class="language-js" data-lang="js"><span style="color:#65737e;">// app/models/post.js
</span><span style="color:#b48ead;">import </span><span style="color:#bf616a;">DS </span><span style="color:#b48ead;">from </span><span>'</span><span style="color:#a3be8c;">ember-data</span><span>';
</span><span>
</span><span style="color:#b48ead;">export default </span><span style="color:#bf616a;">DS</span><span>.</span><span style="color:#bf616a;">Model</span><span>.</span><span style="color:#8fa1b3;">extend</span><span>({
</span><span> title: </span><span style="color:#bf616a;">DS</span><span>.</span><span style="color:#8fa1b3;">attr</span><span>('</span><span style="color:#a3be8c;">string</span><span>'),
</span><span> content: </span><span style="color:#bf616a;">DS</span><span>.</span><span style="color:#8fa1b3;">attr</span><span>('</span><span style="color:#a3be8c;">string</span><span>'),
</span><span> createdAt: </span><span style="color:#bf616a;">DS</span><span>.</span><span style="color:#8fa1b3;">attr</span><span>('</span><span style="color:#a3be8c;">string</span><span>'),
</span><span> updatedAt: </span><span style="color:#bf616a;">DS</span><span>.</span><span style="color:#8fa1b3;">attr</span><span>('</span><span style="color:#a3be8c;">string</span><span>'),
</span><span> socketId: </span><span style="color:#bf616a;">DS</span><span>.</span><span style="color:#8fa1b3;">attr</span><span>('</span><span style="color:#a3be8c;">string</span><span>')
</span><span>});
</span></code></pre>
<p><code>socketId</code> will need to be sent when we create a new post. To do this we need to edit the <code>save</code> action in <code>app/routes/posts/new.js</code>.</p>
<pre data-lang="js" style="background-color:#2b303b;color:#c0c5ce;" class="language-js "><code class="language-js" data-lang="js"><span> </span><span style="color:#65737e;">// ...
</span><span> actions: {
</span><span> save: </span><span style="color:#b48ead;">function</span><span>() {
</span><span> </span><span style="color:#b48ead;">var </span><span style="color:#bf616a;">_this </span><span>= </span><span style="color:#bf616a;">this</span><span>;
</span><span> </span><span style="color:#b48ead;">var </span><span style="color:#bf616a;">post </span><span>= </span><span style="color:#bf616a;">this</span><span>.</span><span style="color:#8fa1b3;">modelFor</span><span>('</span><span style="color:#a3be8c;">posts/new</span><span>')
</span><span>
</span><span> </span><span style="color:#bf616a;">post</span><span>.</span><span style="color:#96b5b4;">set</span><span>('</span><span style="color:#a3be8c;">socketId</span><span>', </span><span style="color:#bf616a;">this</span><span>.</span><span style="color:#bf616a;">pusher</span><span>.</span><span style="color:#96b5b4;">get</span><span>('</span><span style="color:#a3be8c;">socketId</span><span>'));
</span><span>
</span><span> </span><span style="color:#bf616a;">post</span><span>.</span><span style="color:#8fa1b3;">save</span><span>().</span><span style="color:#96b5b4;">then</span><span>(</span><span style="color:#b48ead;">function</span><span>() {
</span><span> </span><span style="color:#bf616a;">_this</span><span>.</span><span style="color:#8fa1b3;">transitionTo</span><span>('</span><span style="color:#a3be8c;">posts.index</span><span>');
</span><span> });
</span><span> }
</span><span> }
</span><span> </span><span style="color:#65737e;">// ...
</span><span>
</span></code></pre>
<p>Notice how we get the model and assign the pusher <code>socketId</code> before saving it to the server.</p>
<h3 id="notification-messages">Notification messages</h3>
<p>Maybe you don't want new data to be pushed on top all of the time. This is a valid opinion, especially when considering the end user - who might be in the middle of reading a particularly long title. They might not appreciate the browser scrolling down all of the time and losing their place.</p>
<p>At this point I think something similar to how Twitter's website deals with new tweets would be ideal. We'll show a little box with a message saying "1 New Post" and allow the user to click it to insert any new posts.</p>
<p>In <code>app/controllers/posts/index.js</code> update the code to the following</p>
<pre data-lang="js" style="background-color:#2b303b;color:#c0c5ce;" class="language-js "><code class="language-js" data-lang="js"><span style="color:#b48ead;">import </span><span style="color:#bf616a;">Ember </span><span style="color:#b48ead;">from </span><span>'</span><span style="color:#a3be8c;">ember</span><span>';
</span><span style="color:#b48ead;">import </span><span>{ </span><span style="color:#bf616a;">Bindings </span><span>} </span><span style="color:#b48ead;">from </span><span>'</span><span style="color:#a3be8c;">ember-pusher/bindings</span><span>';
</span><span>
</span><span style="color:#b48ead;">export default </span><span style="color:#bf616a;">Ember</span><span>.</span><span style="color:#bf616a;">ArrayController</span><span>.</span><span style="color:#8fa1b3;">extend</span><span>(</span><span style="color:#bf616a;">Bindings</span><span>, {
</span><span> sortProperties: ['</span><span style="color:#a3be8c;">createdAt</span><span>'],
</span><span> sortAscending: </span><span style="color:#d08770;">false</span><span>,
</span><span> logPusherEvents: </span><span style="color:#d08770;">true</span><span>,
</span><span> PUSHER_SUBSCRIPTIONS: {
</span><span> posts: ['</span><span style="color:#a3be8c;">new-post</span><span>']
</span><span> },
</span><span> newPosts: [],
</span><span> </span><span style="color:#8fa1b3;">newPostCount</span><span>: </span><span style="color:#b48ead;">function</span><span>() {
</span><span> </span><span style="color:#b48ead;">return </span><span style="color:#bf616a;">this</span><span>.</span><span style="color:#96b5b4;">get</span><span>('</span><span style="color:#a3be8c;">newPosts.length</span><span>');
</span><span> }.</span><span style="color:#8fa1b3;">property</span><span>('</span><span style="color:#a3be8c;">newPosts.length</span><span>'),
</span><span> </span><span style="color:#8fa1b3;">newPostsExist</span><span>: </span><span style="color:#b48ead;">function</span><span>() {
</span><span> </span><span style="color:#b48ead;">return </span><span>!!</span><span style="color:#bf616a;">this</span><span>.</span><span style="color:#96b5b4;">get</span><span>('</span><span style="color:#a3be8c;">newPostCount</span><span>');
</span><span> }.</span><span style="color:#8fa1b3;">property</span><span>('</span><span style="color:#a3be8c;">newPostCount</span><span>'),
</span><span> </span><span style="color:#8fa1b3;">newPostMessage</span><span>: </span><span style="color:#b48ead;">function</span><span>() {
</span><span> </span><span style="color:#b48ead;">var </span><span style="color:#bf616a;">wording </span><span>= (</span><span style="color:#bf616a;">this</span><span>.</span><span style="color:#96b5b4;">get</span><span>('</span><span style="color:#a3be8c;">newPostCount</span><span>') !== </span><span style="color:#d08770;">1</span><span>) ? "</span><span style="color:#a3be8c;">New Posts</span><span>" : "</span><span style="color:#a3be8c;">New Post</span><span>";
</span><span> </span><span style="color:#b48ead;">return </span><span style="color:#bf616a;">this</span><span>.</span><span style="color:#96b5b4;">get</span><span>('</span><span style="color:#a3be8c;">newPostCount</span><span>') + " " + </span><span style="color:#bf616a;">wording</span><span>;
</span><span> }.</span><span style="color:#8fa1b3;">property</span><span>('</span><span style="color:#a3be8c;">newPostCount</span><span>'),
</span><span> actions: {
</span><span> </span><span style="color:#8fa1b3;">newPost</span><span>: </span><span style="color:#b48ead;">function</span><span>(</span><span style="color:#bf616a;">message</span><span>) {
</span><span> </span><span style="color:#b48ead;">if</span><span>(!</span><span style="color:#bf616a;">this</span><span>.</span><span style="color:#bf616a;">store</span><span>.</span><span style="color:#8fa1b3;">hasRecordForId</span><span>('</span><span style="color:#a3be8c;">post</span><span>', </span><span style="color:#bf616a;">message</span><span>.</span><span style="color:#bf616a;">post</span><span>.id)) {
</span><span> </span><span style="color:#bf616a;">this</span><span>.</span><span style="color:#96b5b4;">get</span><span>('</span><span style="color:#a3be8c;">newPosts</span><span>').</span><span style="color:#8fa1b3;">pushObject</span><span>(</span><span style="color:#bf616a;">message</span><span>.</span><span style="color:#bf616a;">post</span><span>);
</span><span> }
</span><span> },
</span><span> </span><span style="color:#8fa1b3;">refresh</span><span>: </span><span style="color:#b48ead;">function</span><span>() {
</span><span> </span><span style="color:#bf616a;">this</span><span>.</span><span style="color:#96b5b4;">get</span><span>('</span><span style="color:#a3be8c;">newPosts</span><span>').</span><span style="color:#96b5b4;">forEach</span><span>(</span><span style="color:#b48ead;">function</span><span>(</span><span style="color:#bf616a;">post</span><span>) {
</span><span> </span><span style="color:#bf616a;">this</span><span>.</span><span style="color:#bf616a;">store</span><span>.</span><span style="color:#96b5b4;">push</span><span>('</span><span style="color:#a3be8c;">post</span><span>', </span><span style="color:#bf616a;">this</span><span>.</span><span style="color:#bf616a;">store</span><span>.</span><span style="color:#96b5b4;">normalize</span><span>('</span><span style="color:#a3be8c;">post</span><span>', </span><span style="color:#bf616a;">post</span><span>));
</span><span> }, </span><span style="color:#bf616a;">this</span><span>);
</span><span> </span><span style="color:#bf616a;">this</span><span>.</span><span style="color:#96b5b4;">set</span><span>('</span><span style="color:#a3be8c;">newPosts</span><span>', []);
</span><span> }
</span><span> }
</span><span>});
</span></code></pre>
<p>On line 11 we have a new property <code>newPosts</code> which by default is an empty array. Every time a new post is sent to Ember we will store it in here.</p>
<p>On line 12-14 we have a computed property called <code>newPostCount</code> which counts the number of posts we have stored in the <code>newPosts</code> array.</p>
<p>On lines 15-17 we have a computed property called <code>newPostsExist</code> which returns a boolean value for our template. The double exclamation mark converts the <code>newPosts</code> length to false if it's 0 and true otherwise.</p>
<p>Lines 18-21 sets up the message we want to display to the user when a new post is available. This will be either "1 New Post" or "x New Posts".</p>
<p>In the <code>newPost</code> action on lines 23-27 instead of pushing the post into the store we put it in the <code>newPosts</code> array for later.</p>
<p>The refresh action on lines 28-33 will be a click event in our template which takes each post and pushes them into our store.</p>
<p>The <code>newPostCount</code> computed property has the following code: <code>newPosts.length</code>. This allows it to watch for when the <code>newPosts</code> array has changed in length. With this we can update the user with how many new posts there are automatically.</p>
<p>Here is the new template in <code>app/templates/posts/index.hbs</code></p>
<pre data-lang="html" style="background-color:#2b303b;color:#c0c5ce;" class="language-html "><code class="language-html" data-lang="html"><span><</span><span style="color:#bf616a;">h3</span><span>>Latest Posts</</span><span style="color:#bf616a;">h3</span><span>>
</span><span>{{#if newPostsExist}}
</span><span> <</span><span style="color:#bf616a;">div </span><span style="color:#d08770;">class</span><span>="</span><span style="color:#a3be8c;">alert alert-info click</span><span>" </span><span style="color:#d08770;">{{action </span><span style="background-color:#bf616a;color:#2b303b;">'</span><span style="color:#d08770;">refresh</span><span style="background-color:#bf616a;color:#2b303b;">'</span><span style="color:#d08770;">}}</span><span>>
</span><span> {{ newPostMessage }}
</span><span> </</span><span style="color:#bf616a;">div</span><span>>
</span><span>{{/if}}
</span><span><</span><span style="color:#bf616a;">table </span><span style="color:#d08770;">class</span><span>="</span><span style="color:#a3be8c;">table table-striped</span><span>">
</span><span> <</span><span style="color:#bf616a;">tr</span><span>>
</span><span> <</span><span style="color:#bf616a;">th </span><span style="color:#d08770;">class</span><span>=</span><span style="color:#a3be8c;">“col-md-3”</span><span>>Title</</span><span style="color:#bf616a;">th</span><span>>
</span><span> <</span><span style="color:#bf616a;">th </span><span style="color:#d08770;">class</span><span>="</span><span style="color:#a3be8c;">col-md-3</span><span>">Created at</</span><span style="color:#bf616a;">th</span><span>>
</span><span> <</span><span style="color:#bf616a;">th </span><span style="color:#d08770;">class</span><span>="</span><span style="color:#a3be8c;">col-md-3</span><span>">Last modified</</span><span style="color:#bf616a;">th</span><span>>
</span><span> <</span><span style="color:#bf616a;">th </span><span style="color:#d08770;">class</span><span>="</span><span style="color:#a3be8c;">col-md-3</span><span>">Actions</</span><span style="color:#bf616a;">th</span><span>>
</span><span> </</span><span style="color:#bf616a;">tr</span><span>>
</span><span>{{#each post in arrangedContent}}
</span><span><</span><span style="color:#bf616a;">tr</span><span>>
</span><span> <</span><span style="color:#bf616a;">td</span><span>>{{ post.title }}</</span><span style="color:#bf616a;">td</span><span>>
</span><span> <</span><span style="color:#bf616a;">td</span><span>>{{ post.createdAt }}</</span><span style="color:#bf616a;">td</span><span>>
</span><span> <</span><span style="color:#bf616a;">td</span><span>>{{ post.updatedAt }}</</span><span style="color:#bf616a;">td</span><span>>
</span><span> <</span><span style="color:#bf616a;">td</span><span>>
</span><span> {{#link-to 'posts.show' post classNames="btn btn-success"}}View{{/link-to}}
</span><span> {{#if session.isAuthenticated}}
</span><span> {{#link-to 'posts.edit' post classNames="btn btn-info"}}Edit{{/link-to}}
</span><span> <</span><span style="color:#bf616a;">button </span><span style="color:#d08770;">{{action </span><span style="background-color:#bf616a;color:#2b303b;">'</span><span style="color:#d08770;">delete</span><span style="background-color:#bf616a;color:#2b303b;">'</span><span> </span><span style="color:#d08770;">post}} class</span><span>="</span><span style="color:#a3be8c;">btn btn-danger</span><span>">Delete</</span><span style="color:#bf616a;">button</span><span>>
</span><span> {{/if}}
</span><span> </</span><span style="color:#bf616a;">td</span><span>>
</span><span></</span><span style="color:#bf616a;">tr</span><span>>
</span><span>{{/each}}
</span><span></</span><span style="color:#bf616a;">table</span><span>>
</span><span>{{#if session.isAuthenticated}}
</span><span> {{#link-to 'posts.new' classNames="btn btn-primary"}}
</span><span> New Post
</span><span> {{/link-to}}
</span><span>{{/if}}
</span></code></pre>
<p>We bind the click event <code>refresh</code> to the new alert so when a user clicks the message they receive the new posts and the alert gets removed.</p>
<p>Feel free to add some CSS or use anchor tags so that the cursor changes on hover and encourages the user to click!</p>
<pre data-lang="css" style="background-color:#2b303b;color:#c0c5ce;" class="language-css "><code class="language-css" data-lang="css"><span style="color:#8fa1b3;">.</span><span style="color:#d08770;">click </span><span>{
</span><span> cursor: pointer;
</span><span> cursor: hand;
</span><span>}
</span></code></pre>
<p><img src="https://i.imgur.com/nQWToRN.png" alt="Final app" /></p>
<h2 id="summary">Summary</h2>
<p>In this post we've covered 2 ways of using Pusher to handle notifications. One where new posts get added directly to the store and another where we hold on to changes and let the user handle updates.</p>
<p>I hope you have enjoyed this series and learned how awesome it is to use Padrino with Ember. Don't forget to check out the full source on <a href="https://github.com/acoustep/padrino-ember-example">Github</a>!</p>
Using Padrino with Ember CLI Part 4: Authorisation2015-03-03T00:00:00+00:002015-03-03T00:00:00+00:00https://www.fullstackstanley.com/articles/using-padrino-with-ember-cli-part-4-authorisation/<p>In the last article I went through how to implement authorisation with Ember-simple-auth and Padrino. The next logical step is to implement authorisation with the same toolset.</p>
<span id="continue-reading"></span>
<p><a href="https://github.com/acoustep/padrino-ember-example">You can view the full source for the application on Github</a></p>
<h2 id="ember">Ember</h2>
<p>On the Ember side of things it couldn't be easier to implement.</p>
<p>All we need to do is open up our configuration (<code>app/config/environment.js</code>) and add the <code>authorizer</code> property to our <code>simple-auth</code> environment variable.</p>
<pre data-lang="js" style="background-color:#2b303b;color:#c0c5ce;" class="language-js "><code class="language-js" data-lang="js"><span> </span><span style="color:#bf616a;">ENV</span><span>['</span><span style="color:#a3be8c;">simple-auth</span><span>'] = {
</span><span> routeAfterAuthentication: '</span><span style="color:#a3be8c;">posts.index</span><span>',
</span><span> routeIfAlreadyAuthenticated: '</span><span style="color:#a3be8c;">posts.index</span><span>',
</span><span> authorizer: '</span><span style="color:#a3be8c;">simple-auth-authorizer:devise</span><span>'
</span><span> };
</span></code></pre>
<p>Let's take a look at the header request for the posts API when we aren't logged in</p>
<p><a href="https://i.imgur.com/43c5W5O.png"><img src="https://i.imgur.com/43c5W5O.png" alt="Not logged in" /></a></p>
<p>Now let's compare that to when we are logged in</p>
<p><a href="https://i.imgur.com/eSBET4k.png"><img src="https://i.imgur.com/eSBET4k.png" alt="Logged in" /></a></p>
<p>As you can see: authorisation is sent with our email and our token. This will occur on every request so it's up to you to decide with Padrino which API requests need guarding or filtering.</p>
<p>Before we move on to the Padrino section of this article I think it's important for you to be aware of one important feature of Ember-data.</p>
<p>By convention Ember will use something similar to this line of code to fetch a specific post.</p>
<pre data-lang="js" style="background-color:#2b303b;color:#c0c5ce;" class="language-js "><code class="language-js" data-lang="js"><span style="color:#bf616a;">store</span><span>.</span><span style="color:#96b5b4;">find</span><span>('</span><span style="color:#a3be8c;">post</span><span>', </span><span style="color:#d08770;">15</span><span>)
</span></code></pre>
<p>This request queries the server if the post hasn't already been loaded. If the user has viewed the list of posts then it will have been. Therefore there will be no authorisation checks. With this in mind make sure that you filter out data in the API rather than in Ember.</p>
<h2 id="padrino">Padrino</h2>
<p>I want to mention a sweet tip when working with APIs in Ruby.</p>
<pre data-lang="ruby" style="background-color:#2b303b;color:#c0c5ce;" class="language-ruby "><code class="language-ruby" data-lang="ruby"><span style="color:#b48ead;">return </span><span style="color:#ebcb8b;">JSON</span><span>.pretty_generate(request.env)
</span></code></pre>
<p>If you can't find something that you think should be in the request there is no nicer way of formatting your request. There's also the <a href="http://ruby-doc.org/stdlib-2.1.1/libdoc/pp/rdoc/PP.html">pretty print</a> library but that will only give you output in your command line.</p>
<p>Ember-simple-auth-devise sends the following to our API when we're logged in</p>
<pre style="background-color:#2b303b;color:#c0c5ce;"><code><span>"Token authentication_token="gDXbsrbDq2H9CKKG42Ts0A", email="admin@admin.co.uk""
</span></code></pre>
<p>It isn't the easiest string to parse but taking a little inspiration from Rail's <a href="https://github.com/rails/rails/blob/25b14b4d3238d5474c60826ee1b359537af987ef/actionpack/lib/action_controller/metal/http_authentication.rb#L406">HttpAuthentication module</a> I came out the other end with this as my <code>models/user.rb</code> file.</p>
<pre data-lang="ruby" style="background-color:#2b303b;color:#c0c5ce;" class="language-ruby "><code class="language-ruby" data-lang="ruby"><span>
</span><span style="color:#8fa1b3;">require </span><span>'</span><span style="color:#a3be8c;">bcrypt</span><span>'
</span><span style="color:#8fa1b3;">require </span><span>'</span><span style="color:#a3be8c;">securerandom</span><span>'
</span><span>
</span><span style="color:#b48ead;">class </span><span style="color:#ebcb8b;">User </span><span style="color:#eff1f5;">< </span><span style="color:#a3be8c;">Sequel::Model
</span><span> </span><span style="color:#ebcb8b;">Sequel</span><span>::</span><span style="color:#ebcb8b;">Model</span><span>.plugin </span><span style="color:#a3be8c;">:timestamps
</span><span>
</span><span> </span><span style="color:#8fa1b3;">attr_reader </span><span style="color:#a3be8c;">:readable_token
</span><span>
</span><span> </span><span style="color:#b48ead;">def </span><span style="color:#8fa1b3;">before_create
</span><span> </span><span style="color:#bf616a;">self</span><span>.password = </span><span style="color:#ebcb8b;">BCrypt</span><span>::</span><span style="color:#ebcb8b;">Password</span><span>.create(</span><span style="color:#bf616a;">self</span><span>.password)
</span><span> generate_authentication_token
</span><span> </span><span style="color:#b48ead;">end
</span><span>
</span><span> </span><span style="color:#b48ead;">def </span><span style="color:#bf616a;">self</span><span>.</span><span style="color:#8fa1b3;">sign_in </span><span style="color:#bf616a;">credentials
</span><span> user = </span><span style="color:#bf616a;">self</span><span>.first(</span><span style="color:#a3be8c;">email:</span><span> credentials.fetch("</span><span style="color:#a3be8c;">email</span><span>", ""))
</span><span> </span><span style="color:#b48ead;">return </span><span style="color:#d08770;">false </span><span style="color:#b48ead;">unless</span><span> user
</span><span>
</span><span> </span><span style="color:#b48ead;">return </span><span style="color:#d08770;">false </span><span style="color:#b48ead;">if </span><span style="color:#ebcb8b;">BCrypt</span><span>::</span><span style="color:#ebcb8b;">Password</span><span>.</span><span style="color:#8fa1b3;">new</span><span>(user.password) != credentials.fetch("</span><span style="color:#a3be8c;">password</span><span>", "")
</span><span>
</span><span> user.generate_authentication_token
</span><span> user.save
</span><span> </span><span style="color:#b48ead;">return</span><span> user
</span><span> </span><span style="color:#b48ead;">end
</span><span>
</span><span> </span><span style="color:#b48ead;">def </span><span style="color:#8fa1b3;">generate_authentication_token</span><span>(</span><span style="color:#bf616a;">user</span><span>=</span><span style="color:#d08770;">false</span><span>)
</span><span> </span><span style="color:#bf616a;">self</span><span>.authentication_token = </span><span style="color:#ebcb8b;">SecureRandom</span><span>.urlsafe_base64(</span><span style="color:#d08770;">nil</span><span>,</span><span style="color:#d08770;">false</span><span>)
</span><span> @</span><span style="color:#bf616a;">readable_token </span><span>= </span><span style="color:#bf616a;">self</span><span>.authentication_token
</span><span> </span><span style="color:#bf616a;">self</span><span>.authentication_token = </span><span style="color:#ebcb8b;">BCrypt</span><span>::</span><span style="color:#ebcb8b;">Password</span><span>.create(</span><span style="color:#bf616a;">self</span><span>.authentication_token)
</span><span> </span><span style="color:#b48ead;">return
</span><span> </span><span style="color:#b48ead;">end
</span><span>
</span><span> </span><span style="color:#b48ead;">def </span><span style="color:#bf616a;">self</span><span>.</span><span style="color:#8fa1b3;">authenticate</span><span>(</span><span style="color:#bf616a;">environment</span><span>, </span><span style="color:#bf616a;">parser</span><span>=</span><span style="color:#bf616a;">AuthorizationStringParser</span><span>)
</span><span> credentials = parser.</span><span style="color:#8fa1b3;">new</span><span>(environment).parsed_string
</span><span> </span><span style="color:#b48ead;">return </span><span style="color:#d08770;">false </span><span style="color:#b48ead;">if</span><span> credentials.</span><span style="color:#96b5b4;">nil?
</span><span> user = </span><span style="color:#bf616a;">self</span><span>.where(</span><span style="color:#a3be8c;">email:</span><span> credentials.fetch("</span><span style="color:#a3be8c;">email</span><span>", "")).first
</span><span> </span><span style="color:#b48ead;">return </span><span style="color:#d08770;">false </span><span style="color:#b48ead;">unless</span><span> user
</span><span> </span><span style="color:#b48ead;">return </span><span style="color:#ebcb8b;">BCrypt</span><span>::</span><span style="color:#ebcb8b;">Password</span><span>.</span><span style="color:#8fa1b3;">new</span><span>(user[</span><span style="color:#a3be8c;">:authentication_token</span><span>]) == credentials.fetch("</span><span style="color:#a3be8c;">authentication_token</span><span>", "")
</span><span> </span><span style="color:#b48ead;">end
</span><span style="color:#b48ead;">end
</span><span>
</span><span style="color:#b48ead;">class </span><span style="color:#ebcb8b;">AuthorizationStringParser
</span><span style="color:#eff1f5;"> </span><span style="color:#8fa1b3;">attr_accessor </span><span style="color:#a3be8c;">:parsed_string</span><span>, </span><span style="color:#a3be8c;">:environment
</span><span> </span><span style="color:#b48ead;">def </span><span style="color:#8fa1b3;">initialize</span><span>(</span><span style="color:#bf616a;">environment</span><span>)
</span><span> @</span><span style="color:#bf616a;">environment </span><span>= environment
</span><span> </span><span style="color:#b48ead;">end
</span><span>
</span><span> </span><span style="color:#b48ead;">def </span><span style="color:#8fa1b3;">parsed_string
</span><span> parsed_string ||= parse_string
</span><span> </span><span style="color:#b48ead;">end
</span><span>
</span><span> </span><span style="color:#8fa1b3;">protected
</span><span>
</span><span> </span><span style="color:#b48ead;">def </span><span style="color:#8fa1b3;">parse_string
</span><span> raw_param_string = environment.fetch("</span><span style="color:#a3be8c;">HTTP_AUTHORIZATION</span><span>", "")
</span><span> raw_param_string.</span><span style="color:#96b5b4;">gsub</span><span>(/</span><span style="color:#96b5b4;">Token\s|"|,</span><span>/, "").split(' ').map { |</span><span style="color:#bf616a;">key_value</span><span>| key_value.split(%r/</span><span style="color:#96b5b4;">=(.+)?</span><span>/) }.</span><span style="color:#96b5b4;">to_h
</span><span> </span><span style="color:#b48ead;">end
</span><span style="color:#b48ead;">end
</span></code></pre>
<p>On lines 32-38 we have the new <code>self.authenticate</code> method which takes the <code>request.env</code> (or any hash for that matter). I chose pass the environment in as <code>request</code> is not available in the model and I didn't feel it was the controller's responsibility to handle parsing the token string.</p>
<p>It's also much nicer to just call <code>User.authenticate request.env</code> and let the model sort out the details. You could argue that it's not the model's responsibility to do this either. If the application was larger I would consider creating a user repository to do it instead.</p>
<p>On line 32 we reference a new class (which is defined on lines 41-56) . This <code>AuthorizationStringParser</code> is responsible for... parsing the authorisation string.</p>
<p>Between 34 and 36 we check if the credentials exist and if the user's email exists in our database. </p>
<p>Finally on line 37 we use <code>Bcrypt</code> to check if the token that's sent is valid.</p>
<p>Now we should open up our posts controller at <code>app/controllers/posts.rb</code> and set the appropriate permissions for our API.</p>
<pre data-lang="ruby" style="background-color:#2b303b;color:#c0c5ce;" class="language-ruby "><code class="language-ruby" data-lang="ruby"><span style="color:#8fa1b3;">require </span><span>'</span><span style="color:#a3be8c;">json</span><span>'
</span><span>
</span><span style="color:#ebcb8b;">Api</span><span>::</span><span style="color:#ebcb8b;">App</span><span>.controllers </span><span style="color:#a3be8c;">:posts</span><span>, </span><span style="color:#a3be8c;">map: </span><span>"</span><span style="color:#a3be8c;">api/v1/posts</span><span>", </span><span style="color:#a3be8c;">conditions: </span><span>{</span><span style="color:#a3be8c;">:protect </span><span>=> </span><span style="color:#d08770;">true</span><span>} </span><span style="color:#b48ead;">do
</span><span>
</span><span> </span><span style="color:#b48ead;">def </span><span style="color:#bf616a;">self</span><span>.</span><span style="color:#8fa1b3;">protect</span><span>(</span><span style="color:#bf616a;">protected</span><span>)
</span><span> condition </span><span style="color:#b48ead;">do
</span><span> </span><span style="color:#b48ead;">unless </span><span style="color:#ebcb8b;">User</span><span>.authenticate request.env
</span><span> halt </span><span style="color:#d08770;">403</span><span>, "</span><span style="color:#a3be8c;">No secrets for you!</span><span>"
</span><span> </span><span style="color:#b48ead;">end
</span><span> </span><span style="color:#b48ead;">end if </span><span style="color:#8fa1b3;">protected
</span><span> </span><span style="color:#b48ead;">end
</span><span>
</span><span> get </span><span style="color:#a3be8c;">:index</span><span>, </span><span style="color:#a3be8c;">map: </span><span>"", </span><span style="color:#a3be8c;">protect: </span><span style="color:#d08770;">false </span><span style="color:#b48ead;">do
</span><span> @</span><span style="color:#bf616a;">posts </span><span>= </span><span style="color:#ebcb8b;">Post</span><span>.all
</span><span> render "</span><span style="color:#a3be8c;">posts/index</span><span>"
</span><span> </span><span style="color:#b48ead;">end
</span><span>
</span><span> post </span><span style="color:#a3be8c;">:create</span><span>, </span><span style="color:#a3be8c;">map: </span><span>"" </span><span style="color:#b48ead;">do
</span><span> parameters = post_params
</span><span> </span><span style="color:#b48ead;">if</span><span> parameters["</span><span style="color:#a3be8c;">post</span><span>"].</span><span style="color:#96b5b4;">nil?
</span><span> </span><span style="color:#b48ead;">return </span><span>'</span><span style="color:#a3be8c;">{}</span><span>'
</span><span> </span><span style="color:#b48ead;">end
</span><span> @</span><span style="color:#bf616a;">post </span><span>= </span><span style="color:#ebcb8b;">Post</span><span>.create parameters["</span><span style="color:#a3be8c;">post</span><span>"]
</span><span> render "</span><span style="color:#a3be8c;">posts/show</span><span>"
</span><span> </span><span style="color:#b48ead;">end
</span><span>
</span><span> get </span><span style="color:#a3be8c;">:show</span><span>, </span><span style="color:#a3be8c;">map: </span><span>"</span><span style="color:#a3be8c;">:id</span><span>" </span><span style="color:#b48ead;">do
</span><span> @</span><span style="color:#bf616a;">post </span><span>= </span><span style="color:#ebcb8b;">Post</span><span>[params[</span><span style="color:#a3be8c;">:id</span><span>]]
</span><span> render "</span><span style="color:#a3be8c;">posts/show</span><span>"
</span><span> </span><span style="color:#b48ead;">end
</span><span>
</span><span> put </span><span style="color:#a3be8c;">:update</span><span>, </span><span style="color:#a3be8c;">map: </span><span>"</span><span style="color:#a3be8c;">:id</span><span>" </span><span style="color:#b48ead;">do
</span><span> @</span><span style="color:#bf616a;">post </span><span>= </span><span style="color:#ebcb8b;">Post</span><span>[params[</span><span style="color:#a3be8c;">:id</span><span>]]
</span><span> </span><span style="color:#b48ead;">if </span><span>@</span><span style="color:#bf616a;">post</span><span>.</span><span style="color:#96b5b4;">nil?
</span><span> </span><span style="color:#b48ead;">return </span><span>'</span><span style="color:#a3be8c;">{}</span><span>'
</span><span> </span><span style="color:#b48ead;">end
</span><span> parameters = post_params
</span><span> @</span><span style="color:#bf616a;">post</span><span>.update parameters["</span><span style="color:#a3be8c;">post</span><span>"]
</span><span> render "</span><span style="color:#a3be8c;">posts/show</span><span>"
</span><span> </span><span style="color:#b48ead;">end
</span><span>
</span><span> delete </span><span style="color:#a3be8c;">:destroy</span><span>, </span><span style="color:#a3be8c;">map: </span><span>"</span><span style="color:#a3be8c;">:id</span><span>" </span><span style="color:#b48ead;">do
</span><span> @</span><span style="color:#bf616a;">post </span><span>= </span><span style="color:#ebcb8b;">Post</span><span>[params[</span><span style="color:#a3be8c;">:id</span><span>]]
</span><span> @</span><span style="color:#bf616a;">post</span><span>.delete </span><span style="color:#b48ead;">unless </span><span>@</span><span style="color:#bf616a;">post</span><span>.</span><span style="color:#96b5b4;">nil?
</span><span> </span><span style="color:#b48ead;">end
</span><span>
</span><span style="color:#b48ead;">end
</span><span>
</span><span style="color:#b48ead;">def </span><span style="color:#8fa1b3;">post_params
</span><span> </span><span style="color:#ebcb8b;">JSON</span><span>.parse(request.body.read)
</span><span style="color:#b48ead;">end
</span></code></pre>
<p>On line 3 we've added a new parameter, <code>conditions: {:protect => true}</code> and on lines 5-11 there's a new <code>self.protect</code> method which will be called before every route in the controller.</p>
<p>This is where we take advantage of our new <code>User.authenticate</code> method and return a 403 error for invalid authorisation requests.</p>
<p>On line 13 and 27 we have disabled protection so that the posts can be accessed with out being logged in. All of the other routes that edit the data require authorisation.</p>
<h3 id="tests">Tests</h3>
<p>We best add some tests for our new authenticate method. Open up <code>test/models/user_test.rb</code> and add the following.</p>
<pre data-lang="ruby" style="background-color:#2b303b;color:#c0c5ce;" class="language-ruby "><code class="language-ruby" data-lang="ruby"><span>
</span><span>it "</span><span style="color:#a3be8c;">returns true when valid</span><span>" </span><span style="color:#b48ead;">do
</span><span> params = { "</span><span style="color:#a3be8c;">email</span><span>" => "</span><span style="color:#a3be8c;">admin@admin.co.uk</span><span>", "</span><span style="color:#a3be8c;">password</span><span>" => "</span><span style="color:#a3be8c;">testpassword</span><span>"}
</span><span> user = </span><span style="color:#ebcb8b;">User</span><span>.sign_in params
</span><span>
</span><span> authenticated = </span><span style="color:#ebcb8b;">User</span><span>.authenticate({"</span><span style="color:#a3be8c;">HTTP_AUTHORIZATION</span><span>" => "</span><span style="color:#a3be8c;">Token authentication_token=</span><span style="color:#96b5b4;">\"</span><span>#{user.readable_token}</span><span style="color:#96b5b4;">\"</span><span style="color:#a3be8c;">, email=</span><span style="color:#96b5b4;">\"</span><span>#{user.email}</span><span style="color:#96b5b4;">\"</span><span>"})
</span><span> assert_equal </span><span style="color:#d08770;">true</span><span>, authenticated
</span><span style="color:#b48ead;">end
</span><span>
</span><span>it "</span><span style="color:#a3be8c;">returns false when invalid</span><span>" </span><span style="color:#b48ead;">do
</span><span> params = { "</span><span style="color:#a3be8c;">email</span><span>" => "</span><span style="color:#a3be8c;">admin@admin.co.uk</span><span>", "</span><span style="color:#a3be8c;">password</span><span>" => "</span><span style="color:#a3be8c;">testpassword</span><span>"}
</span><span> user = </span><span style="color:#ebcb8b;">User</span><span>.sign_in params
</span><span>
</span><span> </span><span style="color:#ebcb8b;">User</span><span>.sign_in params
</span><span>
</span><span> authenticated = </span><span style="color:#ebcb8b;">User</span><span>.authenticate({"</span><span style="color:#a3be8c;">HTTP_AUTHORIZATION</span><span>" => "</span><span style="color:#a3be8c;">Token authentication_token=</span><span style="color:#96b5b4;">\"</span><span>#{user.readable_token}</span><span style="color:#96b5b4;">\"</span><span style="color:#a3be8c;">, email=</span><span style="color:#96b5b4;">\"</span><span>#{user.email}</span><span style="color:#96b5b4;">\"</span><span>"})
</span><span> assert_equal </span><span style="color:#d08770;">false</span><span>, authenticated
</span><span> </span><span style="color:#b48ead;">end
</span><span>
</span><span>it "</span><span style="color:#a3be8c;">returns false if no header is sent</span><span>" </span><span style="color:#b48ead;">do
</span><span> params = { "</span><span style="color:#a3be8c;">email</span><span>" => "</span><span style="color:#a3be8c;">admin@admin.co.uk</span><span>", "</span><span style="color:#a3be8c;">password</span><span>" => "</span><span style="color:#a3be8c;">testpassword</span><span>"}
</span><span> user = </span><span style="color:#ebcb8b;">User</span><span>.sign_in params
</span><span>
</span><span> </span><span style="color:#ebcb8b;">User</span><span>.sign_in params
</span><span>
</span><span> authenticated = </span><span style="color:#ebcb8b;">User</span><span>.authenticate({})
</span><span> assert_equal </span><span style="color:#d08770;">false</span><span>, authenticated
</span><span style="color:#b48ead;">end
</span></code></pre>
<h2 id="tidying-up-ember">Tidying up Ember</h2>
<p>At this point our authorisation is in place but it's hardly a good idea to leave all those buttons for adding, editing and deleting data lying around.</p>
<p>Let's change <code>app/templates/posts/index.hbs</code> to hide these buttons when the user's aren't logged in.</p>
<pre data-lang="html" style="background-color:#2b303b;color:#c0c5ce;" class="language-html "><code class="language-html" data-lang="html"><span><</span><span style="color:#bf616a;">h3</span><span>>Latest Posts</</span><span style="color:#bf616a;">h3</span><span>>
</span><span><</span><span style="color:#bf616a;">table </span><span style="color:#d08770;">class</span><span>="</span><span style="color:#a3be8c;">table table-striped</span><span>">
</span><span> <</span><span style="color:#bf616a;">tr</span><span>>
</span><span> <</span><span style="color:#bf616a;">th </span><span style="color:#d08770;">class</span><span>=</span><span style="color:#a3be8c;">“col-md-3”</span><span>>Title</</span><span style="color:#bf616a;">th</span><span>>
</span><span> <</span><span style="color:#bf616a;">th </span><span style="color:#d08770;">class</span><span>="</span><span style="color:#a3be8c;">col-md-3</span><span>">Created at</</span><span style="color:#bf616a;">th</span><span>>
</span><span> <</span><span style="color:#bf616a;">th </span><span style="color:#d08770;">class</span><span>="</span><span style="color:#a3be8c;">col-md-3</span><span>">Last modified</</span><span style="color:#bf616a;">th</span><span>>
</span><span> <</span><span style="color:#bf616a;">th </span><span style="color:#d08770;">class</span><span>="</span><span style="color:#a3be8c;">col-md-3</span><span>">Actions</</span><span style="color:#bf616a;">th</span><span>>
</span><span> </</span><span style="color:#bf616a;">tr</span><span>>
</span><span>{{#each post in model}}
</span><span><</span><span style="color:#bf616a;">tr</span><span>>
</span><span> <</span><span style="color:#bf616a;">td</span><span>>{{ post.title }}</</span><span style="color:#bf616a;">td</span><span>>
</span><span> <</span><span style="color:#bf616a;">td</span><span>>{{ post.createdAt }}</</span><span style="color:#bf616a;">td</span><span>>
</span><span> <</span><span style="color:#bf616a;">td</span><span>>{{ post.updatedAt }}</</span><span style="color:#bf616a;">td</span><span>>
</span><span> <</span><span style="color:#bf616a;">td</span><span>>
</span><span> {{#link-to 'posts.show' post classNames="btn btn-success"}}View{{/link-to}}
</span><span> {{#if session.isAuthenticated}}
</span><span> {{#link-to 'posts.edit' post classNames="btn btn-info"}}Edit{{/link-to}}
</span><span> <</span><span style="color:#bf616a;">button </span><span style="color:#d08770;">{{action </span><span style="background-color:#bf616a;color:#2b303b;">'</span><span style="color:#d08770;">delete</span><span style="background-color:#bf616a;color:#2b303b;">'</span><span> </span><span style="color:#d08770;">post}} class</span><span>="</span><span style="color:#a3be8c;">btn btn-danger</span><span>">Delete</</span><span style="color:#bf616a;">button</span><span>>
</span><span> {{/if}}
</span><span> </</span><span style="color:#bf616a;">td</span><span>>
</span><span></</span><span style="color:#bf616a;">tr</span><span>>
</span><span>{{/each}}
</span><span></</span><span style="color:#bf616a;">table</span><span>>
</span><span>{{#if session.isAuthenticated}}
</span><span> {{#link-to 'posts.new' classNames="btn btn-primary"}}
</span><span> New Post
</span><span> {{/link-to}}
</span><span>{{/if}}
</span></code></pre>
<p>We have already disabled viewing the new posts route in a previous article. We should do the same for the edit posts, too. Open up <code>app/routes/posts/edit.js</code> and add the <code>AuthenticatedRouteMixin</code>.</p>
<pre data-lang="js" style="background-color:#2b303b;color:#c0c5ce;" class="language-js "><code class="language-js" data-lang="js"><span style="color:#b48ead;">import </span><span style="color:#bf616a;">Ember </span><span style="color:#b48ead;">from </span><span>'</span><span style="color:#a3be8c;">ember</span><span>';
</span><span style="color:#b48ead;">import </span><span style="color:#bf616a;">AuthenticatedRouteMixin </span><span style="color:#b48ead;">from </span><span>'</span><span style="color:#a3be8c;">simple-auth/mixins/authenticated-route-mixin</span><span>';
</span><span>
</span><span style="color:#b48ead;">export default </span><span style="color:#bf616a;">Ember</span><span>.</span><span style="color:#bf616a;">Route</span><span>.</span><span style="color:#8fa1b3;">extend</span><span>(</span><span style="color:#bf616a;">AuthenticatedRouteMixin</span><span>, {
</span><span> </span><span style="color:#8fa1b3;">deactivate</span><span>: </span><span style="color:#b48ead;">function</span><span>() {
</span><span> </span><span style="color:#b48ead;">var </span><span style="color:#bf616a;">model </span><span>= </span><span style="color:#bf616a;">this</span><span>.</span><span style="color:#8fa1b3;">modelFor</span><span>('</span><span style="color:#a3be8c;">posts/edit</span><span>');
</span><span> </span><span style="color:#bf616a;">model</span><span>.</span><span style="color:#8fa1b3;">rollback</span><span>();
</span><span> },
</span><span> actions: {
</span><span> </span><span style="color:#8fa1b3;">save</span><span>: </span><span style="color:#b48ead;">function</span><span>() {
</span><span> </span><span style="color:#b48ead;">var </span><span style="color:#bf616a;">_this </span><span>= </span><span style="color:#bf616a;">this</span><span>;
</span><span> </span><span style="color:#bf616a;">this</span><span>.</span><span style="color:#8fa1b3;">modelFor</span><span>('</span><span style="color:#a3be8c;">posts/edit</span><span>').</span><span style="color:#8fa1b3;">save</span><span>().</span><span style="color:#96b5b4;">then</span><span>(</span><span style="color:#b48ead;">function</span><span>() {
</span><span> </span><span style="color:#bf616a;">_this</span><span>.</span><span style="color:#8fa1b3;">transitionTo</span><span>('</span><span style="color:#a3be8c;">posts.index</span><span>');
</span><span> });
</span><span> }
</span><span> }
</span><span>});
</span></code></pre>
<p>Here are two previews of the system. One logged in and one logged out.
<a href="https://i.imgur.com/Nl5LaD0.png"><img src="https://i.imgur.com/Nl5LaD0.png" alt="Logged in" /></a></p>
<p><a href="https://i.imgur.com/yKSuZyR.png"><img src="https://i.imgur.com/yKSuZyR.png" alt="Logged out" /></a></p>
<h2 id="summary">Summary</h2>
<p>That's it for authorisation with Ember and Padrino! We've covered setting up Ember to use the ember-simple-auth devise authoriser and updated our Padrino application which now reads the request headers to check for valid authentication details.</p>
<p>We've also restricted specific routes to users that are logged in and updated the Ember front-end to reflect these changes.</p>
Using Padrino with Ember CLI Part 3: Authentication2015-02-22T00:00:00+00:002015-02-22T00:00:00+00:00https://www.fullstackstanley.com/articles/using-padrino-with-ember-cli-part-3-authentication/<p>While thinking about what to write for this week's article it occured that I never touched on authentication with Ember and Padrino in my previous articles. So I think this is the perfect excuse to continue with the series! </p>
<span id="continue-reading"></span>
<p>Follow along for creating a Padrino backend that works with Ember-simple-auth and the authentication library Ember-simple-auth-devise.</p>
<p><a href="https://github.com/acoustep/padrino-ember-example">You can view the full source for the application on Github</a></p>
<h2 id="setting-up-the-api">Setting up the API</h2>
<p>Before we start our deep dive into authentication open up your Gemfile, add bcrypt and run <code>bundle install</code>.</p>
<pre data-lang="ruby" style="background-color:#2b303b;color:#c0c5ce;" class="language-ruby "><code class="language-ruby" data-lang="ruby"><span style="color:#8fa1b3;">gem </span><span>'</span><span style="color:#a3be8c;">bcrypt-ruby</span><span>'</span><span style="color:#8fa1b3;">, </span><span>'</span><span style="color:#a3be8c;">~> 3.1.5</span><span>'
</span></code></pre>
<p>We'll start by generating the <code>User</code> model which will have 6 fields: <code>id</code>, <code>email</code>, <code>password</code>, <code>authentication_token</code>, <code>created_at</code> and <code>updated_at</code></p>
<pre data-lang="bash" style="background-color:#2b303b;color:#c0c5ce;" class="language-bash "><code class="language-bash" data-lang="bash"><span style="color:#bf616a;">padrino</span><span> g model User email:string password:string authentication_token:string created_at:datetime updated_at:datetime
</span></code></pre>
<p>Now we can run the migration:</p>
<pre data-lang="bash" style="background-color:#2b303b;color:#c0c5ce;" class="language-bash "><code class="language-bash" data-lang="bash"><span style="color:#bf616a;">rake</span><span> sq:migrate:up
</span></code></pre>
<p>We need to make some changes to the model. Open <code>models/user.rb</code> and add the following code</p>
<pre data-lang="ruby" style="background-color:#2b303b;color:#c0c5ce;" class="language-ruby "><code class="language-ruby" data-lang="ruby"><span style="color:#8fa1b3;">require </span><span>'</span><span style="color:#a3be8c;">bcrypt</span><span>'
</span><span style="color:#8fa1b3;">require </span><span>'</span><span style="color:#a3be8c;">securerandom</span><span>'
</span><span>
</span><span style="color:#b48ead;">class </span><span style="color:#ebcb8b;">User </span><span style="color:#eff1f5;">< </span><span style="color:#a3be8c;">Sequel::Model
</span><span> </span><span style="color:#ebcb8b;">Sequel</span><span>::</span><span style="color:#ebcb8b;">Model</span><span>.plugin </span><span style="color:#a3be8c;">:timestamps
</span><span>
</span><span> </span><span style="color:#8fa1b3;">attr_reader </span><span style="color:#a3be8c;">:readable_token
</span><span>
</span><span> </span><span style="color:#b48ead;">def </span><span style="color:#8fa1b3;">before_create
</span><span> </span><span style="color:#bf616a;">self</span><span>.password = </span><span style="color:#ebcb8b;">BCrypt</span><span>::</span><span style="color:#ebcb8b;">Password</span><span>.create(</span><span style="color:#bf616a;">self</span><span>.password)
</span><span> generate_authentication_token
</span><span> </span><span style="color:#b48ead;">end
</span><span>
</span><span> </span><span style="color:#b48ead;">def </span><span style="color:#bf616a;">self</span><span>.</span><span style="color:#8fa1b3;">sign_in </span><span style="color:#bf616a;">credentials
</span><span> user = </span><span style="color:#bf616a;">self</span><span>.first(</span><span style="color:#a3be8c;">email:</span><span> credentials.fetch("</span><span style="color:#a3be8c;">email</span><span>", ""))
</span><span> </span><span style="color:#b48ead;">return </span><span style="color:#d08770;">false </span><span style="color:#b48ead;">unless</span><span> user
</span><span> </span><span style="color:#b48ead;">return </span><span style="color:#d08770;">false </span><span style="color:#b48ead;">if </span><span style="color:#ebcb8b;">BCrypt</span><span>::</span><span style="color:#ebcb8b;">Password</span><span>.</span><span style="color:#8fa1b3;">new</span><span>(user.password) != credentials.fetch("</span><span style="color:#a3be8c;">password</span><span>", "")
</span><span> user.generate_authentication_token
</span><span> user.save
</span><span> </span><span style="color:#b48ead;">return</span><span> user
</span><span> </span><span style="color:#b48ead;">end
</span><span>
</span><span> </span><span style="color:#b48ead;">def </span><span style="color:#8fa1b3;">generate_authentication_token</span><span>(</span><span style="color:#bf616a;">user</span><span>=</span><span style="color:#d08770;">false</span><span>)
</span><span> </span><span style="color:#bf616a;">self</span><span>.authentication_token = </span><span style="color:#ebcb8b;">SecureRandom</span><span>.urlsafe_base64(</span><span style="color:#d08770;">nil</span><span>,</span><span style="color:#d08770;">false</span><span>)
</span><span> @</span><span style="color:#bf616a;">readable_token </span><span>= </span><span style="color:#bf616a;">self</span><span>.authentication_token
</span><span> </span><span style="color:#bf616a;">self</span><span>.authentication_token = </span><span style="color:#ebcb8b;">BCrypt</span><span>::</span><span style="color:#ebcb8b;">Password</span><span>.create(</span><span style="color:#bf616a;">self</span><span>.authentication_token)
</span><span> </span><span style="color:#b48ead;">return
</span><span> </span><span style="color:#b48ead;">end
</span><span>
</span><span style="color:#b48ead;">end
</span></code></pre>
<p>Just like the <code>Post</code> model we need to include the timestamps plugin so that our <code>created_at</code> and <code>updated_at</code> fields update appropriately.</p>
<p>Second, we need to add a <code>before_create</code> hook which generates an authentication token and hashes our passwords for us.</p>
<p>Third, we have added a static method for signing in. This will take one parameter which is a hash of email and password. It will return an instance of <code>User</code> if valid and <code>false</code> otherwise.</p>
<p>Lastly, we've added a <code>generate_authentication_token</code> which handles the token generation.</p>
<p>Because we should never store the token directly in the database we have to hash it. That puts us in a predicament. How do we read the token but also make sure it's hashed in the database? We could do a <code>before_save</code> hook which automatically hashes unhashed tokens but what if we want to access the token after a save? We would be forced to invalidate a user's session and things start to get messy.</p>
<p>My solution is to create an instance variable named <code>readable_token</code> which gets a copy of the token before it's hashed. This property is not placed in the database but is accessible after a token is generated for the remainder of the session.</p>
<p>This means 3 things:</p>
<ul>
<li>The database always has the correct token because it's always saved straight after generation.</li>
<li>We have access to the token after generation which let's us use the token for the remainder of the session.</li>
<li>If a user sends a separate request we will not have access to the readable token. This encourages us to check authorisation requests against the database hash rather than trying to do a sneaky shortcut and comparing it to the readable token.</li>
</ul>
<p>For token generation we're using <code>SecureRandom.urlsafe_base64(nil, false)</code>. According to <a href="http://stackoverflow.com/questions/6021372/best-way-to-create-unique-token-in-rails">this StackOverflow thread</a> Rails has now deprecated this method in favour of base58. </p>
<p>I've tried to get base58 working but failed miserably. At the time of writing I believe base58 is in the Rails/ActiveSupport master branch where as Padrino uses ActiveSupport 4.2. I may come back to this in the future.</p>
<p>Let's seed a couple of users into our database to test everything's working. By default the Padrino seed command looks for a db/seed.rb file and runs it. I prefer to keep separate files for each model. So in db/seeds.rb put the following</p>
<pre data-lang="ruby" style="background-color:#2b303b;color:#c0c5ce;" class="language-ruby "><code class="language-ruby" data-lang="ruby"><span style="color:#ebcb8b;">Dir</span><span>[</span><span style="color:#96b5b4;">__dir__ </span><span>+ '</span><span style="color:#a3be8c;">/seeds/**/*.rb</span><span>'].each {|</span><span style="color:#bf616a;">file</span><span>| </span><span style="color:#8fa1b3;">require file</span><span>}
</span></code></pre>
<p>And then create <code>db/seeds/user.rb</code>. In this file we'll truncate the database and create two users.</p>
<pre data-lang="ruby" style="background-color:#2b303b;color:#c0c5ce;" class="language-ruby "><code class="language-ruby" data-lang="ruby"><span style="color:#ebcb8b;">User</span><span>.truncate
</span><span>
</span><span style="color:#ebcb8b;">User</span><span>.create({
</span><span> </span><span style="color:#a3be8c;">email: </span><span>"</span><span style="color:#a3be8c;">admin@admin.co.uk</span><span>",
</span><span> </span><span style="color:#a3be8c;">password: </span><span>"</span><span style="color:#a3be8c;">testpassword</span><span>",
</span><span>})
</span><span style="color:#ebcb8b;">User</span><span>.create({
</span><span> </span><span style="color:#a3be8c;">email: </span><span>"</span><span style="color:#a3be8c;">example@example.co.uk</span><span>",
</span><span> </span><span style="color:#a3be8c;">password: </span><span>"</span><span style="color:#a3be8c;">testpassword</span><span>",
</span><span>})
</span></code></pre>
<p><strong>Tip:</strong> Other Sequel methods such as <code>multi_insert</code> will ignore the <code>before_create</code> hook which means we don't get a hashed password or authentication token.</p>
<p>When we run <code>padrino rake seed</code> we can see the data that's been inserted into the database including our hashed passwords and authentication tokens</p>
<p>It's time to move on to the controller <code>app/controllers/users.rb</code>. </p>
<pre data-lang="ruby" style="background-color:#2b303b;color:#c0c5ce;" class="language-ruby "><code class="language-ruby" data-lang="ruby"><span style="color:#ebcb8b;">Api</span><span>::</span><span style="color:#ebcb8b;">App</span><span>.controllers "</span><span style="color:#a3be8c;">api/v1/users</span><span>" </span><span style="color:#b48ead;">do
</span><span>
</span><span> post </span><span style="color:#a3be8c;">:sign_in </span><span style="color:#b48ead;">do
</span><span>
</span><span> user = </span><span style="color:#ebcb8b;">User</span><span>.sign_in params.fetch("</span><span style="color:#a3be8c;">user</span><span>", {})
</span><span>
</span><span> </span><span style="color:#b48ead;">if</span><span> user
</span><span> @</span><span style="color:#bf616a;">status </span><span>= </span><span style="color:#d08770;">201
</span><span> </span><span style="color:#b48ead;">else
</span><span> @</span><span style="color:#bf616a;">message </span><span>= "</span><span style="color:#a3be8c;">Invalid login credentials</span><span>" </span><span style="color:#b48ead;">unless</span><span> user
</span><span> @</span><span style="color:#bf616a;">status </span><span>= </span><span style="color:#d08770;">401
</span><span> </span><span style="color:#b48ead;">end
</span><span>
</span><span> @</span><span style="color:#bf616a;">user </span><span>= user
</span><span> status @</span><span style="color:#bf616a;">status
</span><span> render "</span><span style="color:#a3be8c;">users/sign_in</span><span>"
</span><span> </span><span style="color:#b48ead;">end
</span><span>
</span><span style="color:#b48ead;">end
</span></code></pre>
<p>Ember will be sending a <code>user</code> object which contains <code>email</code> and <code>password</code> key values.</p>
<p>If the user exists we want the request to return a status of 201 for successful creation. If the credentials are invalid we set a message which will be passed to our RABL template.</p>
<p>Let's place the RABL template in <code>app/views/users/sign_in.rabl</code></p>
<pre data-lang="ruby" style="background-color:#2b303b;color:#c0c5ce;" class="language-ruby "><code class="language-ruby" data-lang="ruby"><span>object @</span><span style="color:#bf616a;">user </span><span>=> </span><span style="color:#d08770;">false
</span><span>attributes </span><span style="color:#a3be8c;">:readable_token </span><span>=> </span><span style="color:#a3be8c;">:authentication_token</span><span>, </span><span style="color:#a3be8c;">:email </span><span>=> </span><span style="color:#a3be8c;">:email
</span><span>node (</span><span style="color:#a3be8c;">:message</span><span>) {|</span><span style="color:#bf616a;">m</span><span>| @</span><span style="color:#bf616a;">message </span><span>}
</span></code></pre>
<p>We want to use the user object but we don't want the default behaviour of wrapping "user" around our attributes so we set it to <code>false</code>.</p>
<p>Also notice how we're mapping <code>readable_token</code> to <code>authentication_token</code> for Ember.</p>
<p>The <code>node</code> block let's us add another attribute not associated with the user. This message will host our error message to show the user.</p>
<p>Here's a couple of screenshots of how we can interact with the API via Postman APP and the structure of the JSON returned.</p>
<h3 id="success">Success</h3>
<p><img src="https://i.imgur.com/toncsAo.png" alt="Success" /></p>
<h3 id="failed">Failed</h3>
<p><img src="https://i.imgur.com/eEwOUD0.png" alt="Failed" /></p>
<h2 id="testing">Testing</h2>
<p>So far we've done a shameful amount of testing. Well no more! I think at least for authentication it's important we have a few tests in place for peace of mind.</p>
<p>When we first generated the project we included MiniTest which Padrino has kindly set up for us. </p>
<p>When we run <code>padrino rake test</code> we get a couple of failures due to some helper tests. Go in to <code>test/app/helpers</code> and remove the two files <code>posts_helper_test.rb</code> and <code>users_helper_tests.rb</code>. Running <code>padrino rake test</code> now brings us back to green.</p>
<p>Here is our <code>test/app/controllers/users_controller_test.rb</code></p>
<pre data-lang="ruby" style="background-color:#2b303b;color:#c0c5ce;" class="language-ruby "><code class="language-ruby" data-lang="ruby"><span style="color:#8fa1b3;">require </span><span>'</span><span style="color:#a3be8c;">json</span><span>'
</span><span style="color:#8fa1b3;">require </span><span style="color:#ebcb8b;">File</span><span style="color:#8fa1b3;">.expand_path(</span><span style="color:#ebcb8b;">File</span><span style="color:#8fa1b3;">.dirname(</span><span style="color:#bf616a;">__FILE__</span><span style="color:#8fa1b3;">) </span><span>+ '</span><span style="color:#a3be8c;">/../../test_config.rb</span><span>'</span><span style="color:#8fa1b3;">)
</span><span>
</span><span>describe "</span><span style="color:#a3be8c;">/users</span><span>" </span><span style="color:#b48ead;">do
</span><span> before </span><span style="color:#b48ead;">do
</span><span> </span><span style="color:#ebcb8b;">User</span><span>.create({
</span><span> </span><span style="color:#a3be8c;">email: </span><span>"</span><span style="color:#a3be8c;">admin@admin.co.uk</span><span>",
</span><span> </span><span style="color:#a3be8c;">password: </span><span>"</span><span style="color:#a3be8c;">testpassword</span><span>",
</span><span> })
</span><span> </span><span style="color:#b48ead;">end
</span><span>
</span><span> it "</span><span style="color:#a3be8c;">should authenticate valid credentials</span><span>" </span><span style="color:#b48ead;">do
</span><span> post "</span><span style="color:#a3be8c;">api/v1/users/sign_in</span><span>", {"</span><span style="color:#a3be8c;">user</span><span>" => {"</span><span style="color:#a3be8c;">email</span><span>" => "</span><span style="color:#a3be8c;">admin@admin.co.uk</span><span>", "</span><span style="color:#a3be8c;">password</span><span>" => "</span><span style="color:#a3be8c;">testpassword</span><span>"}}
</span><span> json_response = </span><span style="color:#ebcb8b;">JSON</span><span>.parse last_response.body
</span><span> assert_equal </span><span style="color:#d08770;">nil</span><span>, json_response["</span><span style="color:#a3be8c;">message</span><span>"]
</span><span> assert_equal "</span><span style="color:#a3be8c;">admin@admin.co.uk</span><span>", json_response["</span><span style="color:#a3be8c;">email</span><span>"]
</span><span> assert_includes json_response, "</span><span style="color:#a3be8c;">authentication_token</span><span>"
</span><span> assert_equal </span><span style="color:#d08770;">201</span><span>, last_response.status
</span><span> </span><span style="color:#b48ead;">end
</span><span>
</span><span> it "</span><span style="color:#a3be8c;">should authenticate valid credentials</span><span>" </span><span style="color:#b48ead;">do
</span><span> post "</span><span style="color:#a3be8c;">api/v1/users/sign_in</span><span>", {"</span><span style="color:#a3be8c;">user</span><span>" => {"</span><span style="color:#a3be8c;">email</span><span>" => "</span><span style="color:#a3be8c;">admin@admin.co.uk</span><span>", "</span><span style="color:#a3be8c;">password</span><span>" => "</span><span style="color:#a3be8c;">wrongpassword</span><span>"}}
</span><span> json_response = </span><span style="color:#ebcb8b;">JSON</span><span>.parse last_response.body
</span><span> refute_nil json_response["</span><span style="color:#a3be8c;">message</span><span>"]
</span><span> refute_includes json_response, "</span><span style="color:#a3be8c;">email</span><span>"
</span><span> assert_equal </span><span style="color:#d08770;">401</span><span>, last_response.status
</span><span> </span><span style="color:#b48ead;">end
</span><span style="color:#b48ead;">end
</span></code></pre>
<p>The before block creates a valid User for us to test. From there we have two tests: one for testing valid credentials and one for testing invalid credentials. We test that the correct status and appropriate values are returned.</p>
<p>The <code>test/app/models/user_test.rb</code> is a little longer but still rather simple</p>
<pre data-lang="ruby" style="background-color:#2b303b;color:#c0c5ce;" class="language-ruby "><code class="language-ruby" data-lang="ruby"><span style="color:#8fa1b3;">require </span><span style="color:#ebcb8b;">File</span><span style="color:#8fa1b3;">.expand_path(</span><span style="color:#ebcb8b;">File</span><span style="color:#8fa1b3;">.dirname(</span><span style="color:#bf616a;">__FILE__</span><span style="color:#8fa1b3;">) </span><span>+ '</span><span style="color:#a3be8c;">/../test_config.rb</span><span>'</span><span style="color:#8fa1b3;">)
</span><span>
</span><span>describe "</span><span style="color:#a3be8c;">User Model</span><span>" </span><span style="color:#b48ead;">do
</span><span> before </span><span style="color:#b48ead;">do
</span><span> @</span><span style="color:#bf616a;">user </span><span>= </span><span style="color:#ebcb8b;">User</span><span>.create({
</span><span> </span><span style="color:#a3be8c;">email: </span><span>"</span><span style="color:#a3be8c;">admin@admin.co.uk</span><span>",
</span><span> </span><span style="color:#a3be8c;">password: </span><span>"</span><span style="color:#a3be8c;">testpassword</span><span>",
</span><span> })
</span><span> </span><span style="color:#b48ead;">end
</span><span>
</span><span> it '</span><span style="color:#a3be8c;">can construct a new instance</span><span>' </span><span style="color:#b48ead;">do
</span><span> user = </span><span style="color:#ebcb8b;">User</span><span>.</span><span style="color:#8fa1b3;">new
</span><span> refute_nil user
</span><span> </span><span style="color:#b48ead;">end
</span><span>
</span><span> it '</span><span style="color:#a3be8c;">generates an authentication_token when created</span><span>' </span><span style="color:#b48ead;">do
</span><span> refute_nil @</span><span style="color:#bf616a;">user</span><span>, "</span><span style="color:#a3be8c;">authentication_token</span><span>"
</span><span> refute_nil @</span><span style="color:#bf616a;">user</span><span>, "</span><span style="color:#a3be8c;">readable_token</span><span>"
</span><span> </span><span style="color:#b48ead;">end
</span><span>
</span><span> it '</span><span style="color:#a3be8c;">generates a new authentication_token when signed in</span><span>' </span><span style="color:#b48ead;">do
</span><span> params = { "</span><span style="color:#a3be8c;">email</span><span>" => "</span><span style="color:#a3be8c;">admin@admin.co.uk</span><span>", "</span><span style="color:#a3be8c;">password</span><span>" => "</span><span style="color:#a3be8c;">testpassword</span><span>"}
</span><span> user = </span><span style="color:#ebcb8b;">User</span><span>.sign_in params
</span><span>
</span><span> readable_token = user.readable_token
</span><span> authentication_token = user.authentication_token
</span><span>
</span><span> user = </span><span style="color:#ebcb8b;">User</span><span>.sign_in params
</span><span>
</span><span> refute_equal readable_token, user.readable_token
</span><span> refute_equal authentication_token, user.authentication_token
</span><span> </span><span style="color:#b48ead;">end
</span><span>
</span><span> it '</span><span style="color:#a3be8c;">returns false with invalid credentials</span><span>' </span><span style="color:#b48ead;">do
</span><span> params = { "</span><span style="color:#a3be8c;">email</span><span>" => "</span><span style="color:#a3be8c;">admin@admin.co.uk</span><span>", "</span><span style="color:#a3be8c;">password</span><span>" => "</span><span style="color:#a3be8c;">badpassword</span><span>"}
</span><span> user = </span><span style="color:#ebcb8b;">User</span><span>.sign_in params
</span><span>
</span><span> assert_equal </span><span style="color:#d08770;">false</span><span>, user
</span><span> </span><span style="color:#b48ead;">end
</span><span>
</span><span> it "</span><span style="color:#a3be8c;">returns false when credentials aren't set</span><span>" </span><span style="color:#b48ead;">do
</span><span> params = { }
</span><span> user = </span><span style="color:#ebcb8b;">User</span><span>.sign_in params
</span><span>
</span><span> assert_equal </span><span style="color:#d08770;">false</span><span>, user
</span><span> </span><span style="color:#b48ead;">end
</span><span style="color:#b48ead;">end
</span></code></pre>
<p>With this set of tests we're mostly focusing on the behaviour of the authentication token.</p>
<p>Noteably, on lines 21 to 32 we're checking that signing in a second time regenerates the authentication token.</p>
<p>If you're using the Github repo or you want to see green colours fly by then make sure you install <a href="https://github.com/kern/minitest-reporters">minitest-reports</a>.</p>
<p>At this point we have some basic tests in place and <code>padrino rake test</code> should return green. Our journey with Padrino is complete but Ember is calling us...</p>
<h2 id="ember">Ember</h2>
<p>Change directory in to your Ember app and run the following commands.</p>
<pre style="background-color:#2b303b;color:#c0c5ce;"><code><span>ember install:addon ember-cli-simple-auth
</span><span>ember install:addon ember-cli-simple-auth-devise
</span></code></pre>
<p>Ember simple auth requires an authenticator. We could create a custom one but this article is already long enough and Devise does the job. We're already simulating a Rails CRUD app. Why not simulate a Rails authentication library, too? Besides, it's pretty flexible.</p>
<p>If we go in to <code>config/environment.js</code> we can customise our authentication library to work with our API. Let's add some code just above the <code>return ENV</code>:</p>
<pre data-lang="js" style="background-color:#2b303b;color:#c0c5ce;" class="language-js "><code class="language-js" data-lang="js"><span> </span><span style="color:#bf616a;">ENV</span><span>['</span><span style="color:#a3be8c;">simple-auth-devise</span><span>'] = {
</span><span> serverTokenEndpoint: '</span><span style="color:#a3be8c;">/api/v1/users/sign_in</span><span>',
</span><span> tokenAttributeName: '</span><span style="color:#a3be8c;">authentication_token</span><span>',
</span><span> identificationAttributeName: '</span><span style="color:#a3be8c;">email</span><span>',
</span><span> };
</span><span> </span><span style="color:#bf616a;">ENV</span><span>['</span><span style="color:#a3be8c;">simple-auth</span><span>'] = {
</span><span> routeAfterAuthentication: '</span><span style="color:#a3be8c;">posts.index</span><span>',
</span><span> routeIfAlreadyAuthenticated: '</span><span style="color:#a3be8c;">posts.index</span><span>'
</span><span> };
</span></code></pre>
<ul>
<li><code>serverTokenEndpoint</code> is our API end point for signing in.</li>
<li><code>tokenAttributeName</code> is the name of our authentication token.</li>
<li><code>identificationAttributeName</code> is the field we're sending back with the token to identify the user. In this case we're using the email.</li>
<li><code>routeAfterAuthentication</code> and <code>routeIfAlreadyExists</code> is where to redirect to when necessary. By default this is <code>index</code> but since we made the route of our app <code>posts</code> we have to change it.</li>
</ul>
<p>Time to whip out some more ember-cli commands. This time we need to make a login controller and two routes.</p>
<pre style="background-color:#2b303b;color:#c0c5ce;"><code><span>ember g controller login
</span><span>ember g route application
</span><span>ember g route login
</span></code></pre>
<p>We'll make a login form with help from <a href="https://github.com/simplabs/ember-simple-auth/blob/master/examples/2-errors.html">this error message example</a>.</p>
<p>In <code>app/controllers/login.js</code></p>
<pre data-lang="js" style="background-color:#2b303b;color:#c0c5ce;" class="language-js "><code class="language-js" data-lang="js"><span style="color:#b48ead;">import </span><span style="color:#bf616a;">LoginControllerMixin </span><span style="color:#b48ead;">from </span><span>'</span><span style="color:#a3be8c;">simple-auth/mixins/login-controller-mixin</span><span>';
</span><span style="color:#b48ead;">import </span><span style="color:#bf616a;">Ember </span><span style="color:#b48ead;">from </span><span>'</span><span style="color:#a3be8c;">ember</span><span>';
</span><span>
</span><span style="color:#b48ead;">export default </span><span style="color:#bf616a;">Ember</span><span>.</span><span style="color:#bf616a;">Controller</span><span>.</span><span style="color:#8fa1b3;">extend</span><span>(</span><span style="color:#bf616a;">LoginControllerMixin</span><span>, {
</span><span> authenticator: '</span><span style="color:#a3be8c;">simple-auth-authenticator:devise</span><span>',
</span><span> actions: {
</span><span> </span><span style="color:#65737e;">// display an error when authentication fails
</span><span> </span><span style="color:#8fa1b3;">authenticate</span><span>: </span><span style="color:#b48ead;">function</span><span>() {
</span><span> </span><span style="color:#b48ead;">var </span><span style="color:#bf616a;">_this </span><span>= </span><span style="color:#bf616a;">this</span><span>;
</span><span> </span><span style="color:#bf616a;">this</span><span>.</span><span style="color:#8fa1b3;">_super</span><span>().</span><span style="color:#96b5b4;">then</span><span>(</span><span style="color:#d08770;">null</span><span>, </span><span style="color:#b48ead;">function</span><span>(</span><span style="color:#bf616a;">error</span><span>) {
</span><span> </span><span style="color:#bf616a;">_this</span><span>.</span><span style="color:#96b5b4;">set</span><span>('</span><span style="color:#a3be8c;">errorMessage</span><span>', </span><span style="color:#bf616a;">error</span><span>.</span><span style="color:#bf616a;">message</span><span>);
</span><span> });
</span><span> }
</span><span> }
</span><span>});
</span></code></pre>
<p>We add the <code>LoginControllerMixin</code> so that this controller will have the methods for authenticating. We override the authenticate action so that we can set our custom error message in the template.</p>
<p>In <code>app/routes/application.js</code> we need to include the <code>ApplicationRouteMixin</code></p>
<pre data-lang="js" style="background-color:#2b303b;color:#c0c5ce;" class="language-js "><code class="language-js" data-lang="js"><span style="color:#b48ead;">import </span><span style="color:#bf616a;">ApplicationRouteMixin </span><span style="color:#b48ead;">from </span><span>'</span><span style="color:#a3be8c;">simple-auth/mixins/application-route-mixin</span><span>';
</span><span style="color:#b48ead;">import </span><span style="color:#bf616a;">Ember </span><span style="color:#b48ead;">from </span><span>'</span><span style="color:#a3be8c;">ember</span><span>';
</span><span>
</span><span style="color:#b48ead;">export default </span><span style="color:#bf616a;">Ember</span><span>.</span><span style="color:#bf616a;">Route</span><span>.</span><span style="color:#8fa1b3;">extend</span><span>(</span><span style="color:#bf616a;">ApplicationRouteMixin</span><span>);
</span></code></pre>
<p>Now we need to change the <code>setupController</code> inside of <code>app/routes/login.js</code> to remove previous error messages. If we dont then when a user fails logging in, goes to another route and then returns to login they will see the previous error message. </p>
<pre data-lang="js" style="background-color:#2b303b;color:#c0c5ce;" class="language-js "><code class="language-js" data-lang="js"><span style="color:#b48ead;">import </span><span style="color:#bf616a;">Ember </span><span style="color:#b48ead;">from </span><span>'</span><span style="color:#a3be8c;">ember</span><span>';
</span><span>
</span><span style="color:#b48ead;">export default </span><span style="color:#bf616a;">Ember</span><span>.</span><span style="color:#bf616a;">Route</span><span>.</span><span style="color:#8fa1b3;">extend</span><span>({
</span><span> </span><span style="color:#8fa1b3;">setupController</span><span>: </span><span style="color:#b48ead;">function</span><span>(</span><span style="color:#bf616a;">controller</span><span>, </span><span style="color:#bf616a;">model</span><span>) {
</span><span> </span><span style="color:#bf616a;">controller</span><span>.</span><span style="color:#96b5b4;">set</span><span>('</span><span style="color:#a3be8c;">errorMessage</span><span>', </span><span style="color:#d08770;">null</span><span>);
</span><span> }
</span><span>});
</span></code></pre>
<p>It's time we made a navigation bar. Make the partial <code>app/templates/-navigation.hbs</code></p>
<pre data-lang="html" style="background-color:#2b303b;color:#c0c5ce;" class="language-html "><code class="language-html" data-lang="html"><span><</span><span style="color:#bf616a;">nav </span><span style="color:#d08770;">class</span><span>="</span><span style="color:#a3be8c;">navbar navbar-default</span><span>">
</span><span> </span><span style="color:#65737e;"><!-- Collect the nav links, forms, and other content for toggling -->
</span><span> <</span><span style="color:#bf616a;">div </span><span style="color:#d08770;">class</span><span>="</span><span style="color:#a3be8c;">collapse navbar-collapse</span><span>" </span><span style="color:#8fa1b3;">id</span><span>="</span><span style="color:#a3be8c;">bs-example-navbar-collapse-1</span><span>">
</span><span> <</span><span style="color:#bf616a;">ul </span><span style="color:#d08770;">class</span><span>="</span><span style="color:#a3be8c;">nav navbar-nav</span><span>">
</span><span> {{#link-to 'posts.index' tagName='li'}}
</span><span> {{#link-to 'posts.index'}}
</span><span> Posts
</span><span> {{/link-to}}
</span><span> {{/link-to}}
</span><span> {{#if session.isAuthenticated}}
</span><span> <</span><span style="color:#bf616a;">li</span><span>><</span><span style="color:#bf616a;">a </span><span style="color:#d08770;">{{ action </span><span style="background-color:#bf616a;color:#2b303b;">'</span><span style="color:#d08770;">invalidateSession</span><span style="background-color:#bf616a;color:#2b303b;">'</span><span> </span><span style="color:#d08770;">}}</span><span>>Logout</</span><span style="color:#bf616a;">a</span><span>></</span><span style="color:#bf616a;">li</span><span>>
</span><span> {{else}}
</span><span> {{#link-to 'login' tagName='li'}}
</span><span> {{#link-to 'login'}}
</span><span> Login
</span><span> {{/link-to}}
</span><span> {{/link-to}}
</span><span> {{/if}}
</span><span> </</span><span style="color:#bf616a;">ul</span><span>>
</span><span> </</span><span style="color:#bf616a;">div</span><span>></span><span style="color:#65737e;"><!-- /.navbar-collapse -->
</span><span></</span><span style="color:#bf616a;">nav</span><span>>
</span></code></pre>
<p>We have an if statement (<code>if session.isAuthenticated</code>) which let's us show different links depending on whether the user is logged in.</p>
<p>Notice the nested <code>link-to</code> tags. Ember adds an active class to the <code>link-to</code> helpers that match the current route but Bootstrap expects this on the list element rather than the anchor tag. The nested <code>link-to</code> resolves this issue for us nicely.</p>
<p>Time for us to update our <code>app/templates/application.hbs</code> to include our new partial</p>
<pre data-lang="html" style="background-color:#2b303b;color:#c0c5ce;" class="language-html "><code class="language-html" data-lang="html"><span><</span><span style="color:#bf616a;">div </span><span style="color:#d08770;">class</span><span>="</span><span style="color:#a3be8c;">row</span><span>">
</span><span> <</span><span style="color:#bf616a;">div </span><span style="color:#d08770;">class</span><span>="</span><span style="color:#a3be8c;">col-md-12</span><span>">
</span><span> <</span><span style="color:#bf616a;">h1</span><span>>Welcome to Ember with Padrino</</span><span style="color:#bf616a;">h1</span><span>>
</span><span> {{partial "navigation"}}
</span><span> </</span><span style="color:#bf616a;">div</span><span>>
</span><span></</span><span style="color:#bf616a;">div</span><span>>
</span><span><</span><span style="color:#bf616a;">div </span><span style="color:#d08770;">class</span><span>="</span><span style="color:#a3be8c;">row</span><span>">
</span><span> <</span><span style="color:#bf616a;">div </span><span style="color:#d08770;">class</span><span>="</span><span style="color:#a3be8c;">col-md-12</span><span>">
</span><span> {{outlet}}
</span><span> </</span><span style="color:#bf616a;">div</span><span>>
</span><span></</span><span style="color:#bf616a;">div</span><span>>
</span></code></pre>
<p><img src="https://i.imgur.com/174LkRt.png" alt="Screenshot of the new posts page" /></p>
<p>Finally we can create our Login form. We're almost at the finish line guys!</p>
<pre data-lang="html" style="background-color:#2b303b;color:#c0c5ce;" class="language-html "><code class="language-html" data-lang="html"><span><</span><span style="color:#bf616a;">h2</span><span>>Login</</span><span style="color:#bf616a;">h2</span><span>>
</span><span><</span><span style="color:#bf616a;">form </span><span style="color:#d08770;">{{action </span><span style="background-color:#bf616a;color:#2b303b;">'</span><span style="color:#d08770;">authenticate</span><span style="background-color:#bf616a;color:#2b303b;">'</span><span> </span><span style="color:#d08770;">on</span><span>='</span><span style="color:#a3be8c;">submit</span><span>'</span><span style="color:#d08770;">}} class</span><span>="</span><span style="color:#a3be8c;">horizontal</span><span>">
</span><span> {{#if errorMessage}}
</span><span> <</span><span style="color:#bf616a;">div </span><span style="color:#d08770;">class</span><span>="</span><span style="color:#a3be8c;">alert alert-danger</span><span>">
</span><span> <</span><span style="color:#bf616a;">p</span><span>>
</span><span> <</span><span style="color:#bf616a;">strong</span><span>>Login failed:</</span><span style="color:#bf616a;">strong</span><span>> <</span><span style="color:#bf616a;">code</span><span>>{{errorMessage}}</</span><span style="color:#bf616a;">code</span><span>>
</span><span> </</span><span style="color:#bf616a;">p</span><span>>
</span><span> </</span><span style="color:#bf616a;">div</span><span>>
</span><span> {{/if}}
</span><span> <</span><span style="color:#bf616a;">div </span><span style="color:#d08770;">class</span><span>="</span><span style="color:#a3be8c;">form-group</span><span>">
</span><span> <</span><span style="color:#bf616a;">label </span><span style="color:#d08770;">for</span><span>="</span><span style="color:#a3be8c;">identification</span><span>" </span><span style="color:#d08770;">class</span><span>="</span><span style="color:#a3be8c;">control-label</span><span>">Email</</span><span style="color:#bf616a;">label</span><span>>
</span><span> {{input value=identification placeholder='Enter Email' classNames="form-control"}}
</span><span> </</span><span style="color:#bf616a;">div</span><span>>
</span><span> <</span><span style="color:#bf616a;">div </span><span style="color:#d08770;">class</span><span>="</span><span style="color:#a3be8c;">form-group</span><span>">
</span><span> <</span><span style="color:#bf616a;">label </span><span style="color:#d08770;">for</span><span>="</span><span style="color:#a3be8c;">password</span><span>" </span><span style="color:#d08770;">class</span><span>="</span><span style="color:#a3be8c;">control-label</span><span>">Password</</span><span style="color:#bf616a;">label</span><span>>
</span><span> {{input value=password placeholder='Enter Password' type='password' classNames="form-control"}}
</span><span> </</span><span style="color:#bf616a;">div</span><span>>
</span><span> <</span><span style="color:#bf616a;">div </span><span style="color:#d08770;">class</span><span>="</span><span style="color:#a3be8c;">form-group</span><span>">
</span><span> <</span><span style="color:#bf616a;">button </span><span style="color:#d08770;">type</span><span>="</span><span style="color:#a3be8c;">submit</span><span>" </span><span style="color:#d08770;">class</span><span>="</span><span style="color:#a3be8c;">btn btn-primary</span><span>">Login</</span><span style="color:#bf616a;">button</span><span>>
</span><span> </</span><span style="color:#bf616a;">div</span><span>>
</span><span></</span><span style="color:#bf616a;">form</span><span>>
</span></code></pre>
<p><img src="https://i.imgur.com/SfdqI4y.png" alt="Screenshot of the login form" /></p>
<p>Any method route that requires authentication can now include the <code>AuthenticationRouteMixin</code> mixin.</p>
<p>As an example. Change the <code>app/routes/posts/new.js</code> to the following</p>
<pre data-lang="js" style="background-color:#2b303b;color:#c0c5ce;" class="language-js "><code class="language-js" data-lang="js"><span style="color:#b48ead;">import </span><span style="color:#bf616a;">Ember </span><span style="color:#b48ead;">from </span><span>'</span><span style="color:#a3be8c;">ember</span><span>';
</span><span style="color:#b48ead;">import </span><span style="color:#bf616a;">AuthenticatedRouteMixin </span><span style="color:#b48ead;">from </span><span>'</span><span style="color:#a3be8c;">simple-auth/mixins/authenticated-route-mixin</span><span>';
</span><span>
</span><span style="color:#b48ead;">export default </span><span style="color:#bf616a;">Ember</span><span>.</span><span style="color:#bf616a;">Route</span><span>.</span><span style="color:#8fa1b3;">extend</span><span>(</span><span style="color:#bf616a;">AuthenticatedRouteMixin</span><span>, {
</span><span> </span><span style="color:#8fa1b3;">model</span><span>: </span><span style="color:#b48ead;">function</span><span>() {
</span><span> </span><span style="color:#b48ead;">return </span><span style="color:#bf616a;">this</span><span>.</span><span style="color:#bf616a;">store</span><span>.</span><span style="color:#8fa1b3;">createRecord</span><span>('</span><span style="color:#a3be8c;">post</span><span>');
</span><span> },
</span><span> </span><span style="color:#8fa1b3;">deactivate</span><span>: </span><span style="color:#b48ead;">function</span><span>() {
</span><span> </span><span style="color:#b48ead;">var </span><span style="color:#bf616a;">model </span><span>= </span><span style="color:#bf616a;">this</span><span>.</span><span style="color:#8fa1b3;">modelFor</span><span>('</span><span style="color:#a3be8c;">posts/new</span><span>');
</span><span>
</span><span> </span><span style="color:#b48ead;">if</span><span>(</span><span style="color:#bf616a;">model</span><span>.</span><span style="color:#96b5b4;">get</span><span>('</span><span style="color:#a3be8c;">isNew</span><span>')) {
</span><span> </span><span style="color:#bf616a;">model</span><span>.</span><span style="color:#8fa1b3;">destroyRecord</span><span>();
</span><span> }
</span><span> },
</span><span> actions: {
</span><span> </span><span style="color:#8fa1b3;">save</span><span>: </span><span style="color:#b48ead;">function</span><span>() {
</span><span> </span><span style="color:#b48ead;">var </span><span style="color:#bf616a;">_this </span><span>= </span><span style="color:#bf616a;">this</span><span>;
</span><span> </span><span style="color:#bf616a;">this</span><span>.</span><span style="color:#8fa1b3;">modelFor</span><span>('</span><span style="color:#a3be8c;">posts/new</span><span>').</span><span style="color:#8fa1b3;">save</span><span>().</span><span style="color:#96b5b4;">then</span><span>(</span><span style="color:#b48ead;">function</span><span>() {
</span><span> </span><span style="color:#bf616a;">_this</span><span>.</span><span style="color:#8fa1b3;">transitionTo</span><span>('</span><span style="color:#a3be8c;">posts.index</span><span>');
</span><span> });
</span><span> }
</span><span> }
</span><span>});
</span></code></pre>
<p>Now if we try to access the <code>posts/new</code> page without logging in we will be redirected to the login form.</p>
<h2 id="is-this-production-ready">Is this production ready?</h2>
<p><img src="https://i.imgur.com/BilqB9d.gif" alt="No" /></p>
<p>This article is merely proof of concept. There are a few reasons I would not recommend using this for anything you need to keep secure.</p>
<p>This post only covers authentication and not authorization. The authentication part of ember-simple-auth only covers the retrieval of a valid token from a successful log in. It will not check future requests for validity! That's where authorisation comes in which is not covered in this article.</p>
<p>I've also gone and rolled my own authentication in Padrino which is not ideal. I have not covered validation of users, making sure that email addresses are unique etc. Your best bet is to try to get Warden working with Padrino for a proper log in system.</p>
<p>If you're serious about securing your API, read <a href="http://stackoverflow.com/questions/18605294/is-devises-token-authenticatable-secure/18695244#18695244">this StackOverflow post</a> which gives you a list of best practices for securing your tokens.</p>
<h2 id="summary">Summary</h2>
<p>This was a lot longer than I predicted. I guess I can only blame myself for trying to include both Padrino and Ember in one article!</p>
<p>I have covered so much in this article and while I can't speak for you, I can say it's been quite the learning experience for myself.</p>
<p>We've covered the security precautions and ramifications of using Ember-simple-auth for authentication, how to implement a Padrino API compatible with the Ember-simple-auth-devise authentication library and giving users feedback on their incorrect logins.</p>
<p>We also touched on using MiniTest with Padrino, generating secure tokens in Ruby and restricting access to specific routes with Ember-simple-auth.</p>
<p>Next up: <a href="/articles/using-padrino-with-ember-cli-part-4-authorisation">Part 4: Authorization</a></p>
Using Padrino with Ember CLI Part 22015-02-15T00:00:00+00:002015-02-15T00:00:00+00:00https://www.fullstackstanley.com/articles/using-padrino-with-ember-cli-part-2/<p>You can view the full source for this article on <a href="https://github.com/acoustep/padrino-ember-example">Github</a>.</p>
<p>I am using the following library versions for this tutorial:</p>
<ul>
<li>IO.js v1.2.0</li>
<li>NPM 2.5.1</li>
<li>watchman 3.0.0 (Installed with homebrew)</li>
<li>Ember 1.8.1</li>
<li>Ember Data 1.0.0-beta.14.1</li>
<li>jQuery 1.11.2</li>
<li>Handlebars 1.3.0</li>
</ul>
<span id="continue-reading"></span>
<p>Before starting make sure you’re in your <code>Blog</code> directory and not <code>Blog/API</code>.</p>
<p>To create our Ember application we'll use <code>ember new app</code> and then cd into it.</p>
<p>Run <code>ember serve --proxy http://localhost:3000</code> so that we can use our API. The proxy URL should match our Padrino API. By default the port for Padrino is 3000.</p>
<h2 id="bootstrap">Bootstrap</h2>
<p>I’m adding Bootstrap in <code>app/index.html</code> and changing the templates to use bootstrap as well. This is entirely optional. Feel free to use your own CSS / preferred CSS Framework.</p>
<pre data-lang="html" style="background-color:#2b303b;color:#c0c5ce;" class="language-html "><code class="language-html" data-lang="html"><span><!</span><span style="color:#b48ead;">DOCTYPE </span><span style="color:#d08770;">html</span><span>>
</span><span><</span><span style="color:#bf616a;">html</span><span>>
</span><span> <</span><span style="color:#bf616a;">head</span><span>>
</span><span> <</span><span style="color:#bf616a;">meta </span><span style="color:#d08770;">charset</span><span>="</span><span style="color:#a3be8c;">utf-8</span><span>">
</span><span> <</span><span style="color:#bf616a;">meta </span><span style="color:#d08770;">http-equiv</span><span>="</span><span style="color:#a3be8c;">X-UA-Compatible</span><span>" </span><span style="color:#d08770;">content</span><span>="</span><span style="color:#a3be8c;">IE=edge</span><span>">
</span><span> <</span><span style="color:#bf616a;">title</span><span>>App</</span><span style="color:#bf616a;">title</span><span>>
</span><span> <</span><span style="color:#bf616a;">meta </span><span style="color:#d08770;">name</span><span>="</span><span style="color:#a3be8c;">description</span><span>" </span><span style="color:#d08770;">content</span><span>="">
</span><span> <</span><span style="color:#bf616a;">meta </span><span style="color:#d08770;">name</span><span>="</span><span style="color:#a3be8c;">viewport</span><span>" </span><span style="color:#d08770;">content</span><span>="</span><span style="color:#a3be8c;">width=device-width, initial-scale=1</span><span>">
</span><span>
</span><span> {{content-for 'head'}}
</span><span>
</span><span> <</span><span style="color:#bf616a;">link </span><span style="color:#d08770;">rel</span><span>="</span><span style="color:#a3be8c;">stylesheet</span><span>" </span><span style="color:#d08770;">href</span><span>="</span><span style="color:#a3be8c;">//maxcdn.bootstrapcdn.com/bootstrap/3.3.2/css/bootstrap.min.css</span><span>">
</span><span> <</span><span style="color:#bf616a;">link </span><span style="color:#d08770;">rel</span><span>="</span><span style="color:#a3be8c;">stylesheet</span><span>" </span><span style="color:#d08770;">href</span><span>="</span><span style="color:#a3be8c;">assets/vendor.css</span><span>">
</span><span> <</span><span style="color:#bf616a;">link </span><span style="color:#d08770;">rel</span><span>="</span><span style="color:#a3be8c;">stylesheet</span><span>" </span><span style="color:#d08770;">href</span><span>="</span><span style="color:#a3be8c;">assets/app.css</span><span>">
</span><span>
</span><span> {{content-for 'head-footer'}}
</span><span> </</span><span style="color:#bf616a;">head</span><span>>
</span><span> <</span><span style="color:#bf616a;">body</span><span>>
</span><span> <</span><span style="color:#bf616a;">div </span><span style="color:#d08770;">class</span><span>="</span><span style="color:#a3be8c;">container</span><span>">
</span><span> <</span><span style="color:#bf616a;">div </span><span style="color:#8fa1b3;">id</span><span>="</span><span style="color:#a3be8c;">application</span><span>">
</span><span> </</span><span style="color:#bf616a;">div</span><span>>
</span><span> </</span><span style="color:#bf616a;">div</span><span>>
</span><span> {{content-for 'body'}}
</span><span>
</span><span> <</span><span style="color:#bf616a;">script </span><span style="color:#d08770;">src</span><span>="</span><span style="color:#a3be8c;">assets/vendor.js</span><span>"></</span><span style="color:#bf616a;">script</span><span>>
</span><span> <</span><span style="color:#bf616a;">script </span><span style="color:#d08770;">src</span><span>="</span><span style="color:#a3be8c;">assets/app.js</span><span>"></</span><span style="color:#bf616a;">script</span><span>>
</span><span>
</span><span> {{content-for 'body-footer'}}
</span><span> </</span><span style="color:#bf616a;">body</span><span>>
</span><span></</span><span style="color:#bf616a;">html</span><span>>
</span></code></pre>
<p>In <code>app/app.js</code> we can add the <code>rootElement</code> parameter so that our Ember application gets loaded inside of the container.</p>
<pre data-lang="js" style="background-color:#2b303b;color:#c0c5ce;" class="language-js "><code class="language-js" data-lang="js"><span style="color:#b48ead;">var </span><span style="color:#bf616a;">App </span><span>= </span><span style="color:#bf616a;">Ember</span><span>.</span><span style="color:#bf616a;">Application</span><span>.</span><span style="color:#8fa1b3;">extend</span><span>({
</span><span> modulePrefix: </span><span style="color:#bf616a;">config</span><span>.</span><span style="color:#bf616a;">modulePrefix</span><span>,
</span><span> podModulePrefix: </span><span style="color:#bf616a;">config</span><span>.</span><span style="color:#bf616a;">podModulePrefix</span><span>,
</span><span> Resolver: </span><span style="color:#bf616a;">Resolver</span><span>,
</span><span> rootElement: '</span><span style="color:#a3be8c;">#application</span><span>'
</span><span>});
</span></code></pre>
<p>Finally, in <code>app/templates/application.hbs</code> we’ll add some rows and columns to make use of Bootstrap’s responsive grid.</p>
<pre data-lang="html" style="background-color:#2b303b;color:#c0c5ce;" class="language-html "><code class="language-html" data-lang="html"><span><</span><span style="color:#bf616a;">div </span><span style="color:#d08770;">class</span><span>="</span><span style="color:#a3be8c;">row</span><span>">
</span><span> <</span><span style="color:#bf616a;">div </span><span style="color:#d08770;">class</span><span>="</span><span style="color:#a3be8c;">col-md-12</span><span>">
</span><span> <</span><span style="color:#bf616a;">h2 </span><span style="color:#8fa1b3;">id</span><span>="</span><span style="color:#a3be8c;">title</span><span>">Welcome to Ember with Padrino</</span><span style="color:#bf616a;">h2</span><span>>
</span><span> </</span><span style="color:#bf616a;">div</span><span>>
</span><span></</span><span style="color:#bf616a;">div</span><span>>
</span><span><</span><span style="color:#bf616a;">div </span><span style="color:#d08770;">class</span><span>="</span><span style="color:#a3be8c;">row</span><span>">
</span><span> <</span><span style="color:#bf616a;">div </span><span style="color:#d08770;">class</span><span>="</span><span style="color:#a3be8c;">col-md-12</span><span>">
</span><span> {{outlet}}
</span><span> </</span><span style="color:#bf616a;">div</span><span>>
</span><span></</span><span style="color:#bf616a;">div</span><span>>
</span></code></pre>
<h3 id="routes">Routes</h3>
<p>In <code>app/router.js</code> add the following routes:</p>
<pre data-lang="js" style="background-color:#2b303b;color:#c0c5ce;" class="language-js "><code class="language-js" data-lang="js"><span style="color:#bf616a;">Router</span><span>.</span><span style="color:#8fa1b3;">map</span><span>(</span><span style="color:#b48ead;">function</span><span>() {
</span><span> </span><span style="color:#bf616a;">this</span><span>.</span><span style="color:#8fa1b3;">resource</span><span>('</span><span style="color:#a3be8c;">posts</span><span>', {path: '</span><span style="color:#a3be8c;">/</span><span>'}, </span><span style="color:#b48ead;">function</span><span>() {
</span><span> </span><span style="color:#bf616a;">this</span><span>.</span><span style="color:#8fa1b3;">resource</span><span>('</span><span style="color:#a3be8c;">posts.new</span><span>', {path: '</span><span style="color:#a3be8c;">/post/new</span><span>'});
</span><span> </span><span style="color:#bf616a;">this</span><span>.</span><span style="color:#8fa1b3;">resource</span><span>('</span><span style="color:#a3be8c;">posts.show</span><span>', {path: '</span><span style="color:#a3be8c;">/post/:post_id</span><span>'});
</span><span> </span><span style="color:#bf616a;">this</span><span>.</span><span style="color:#8fa1b3;">resource</span><span>('</span><span style="color:#a3be8c;">posts.edit</span><span>', {path: '</span><span style="color:#a3be8c;">/post/:post_id/edit</span><span>'});
</span><span> });
</span><span>});
</span></code></pre>
<p>This is what our Route should look like in Ember Inspector:</p>
<p><a href="https://i.imgur.com/gDaAlCR.png"><img src="https://i.imgur.com/gDaAlCR.png" alt="Ember Routes" /></a></p>
<h3 id="adapter">Adapter</h3>
<p>We can set up the application adapter by running the following command</p>
<pre style="background-color:#2b303b;color:#c0c5ce;"><code><span>ember g adapter application
</span></code></pre>
<p>This will make a new file in <code>app/adapters/application.js</code>. Let’s change the code to work with our API:</p>
<pre data-lang="js" style="background-color:#2b303b;color:#c0c5ce;" class="language-js "><code class="language-js" data-lang="js"><span style="color:#b48ead;">import </span><span style="color:#bf616a;">DS </span><span style="color:#b48ead;">from </span><span>'</span><span style="color:#a3be8c;">ember-data</span><span>';
</span><span>
</span><span style="color:#b48ead;">export default </span><span style="color:#bf616a;">DS</span><span>.</span><span style="color:#bf616a;">ActiveModelAdapter</span><span>.</span><span style="color:#8fa1b3;">extend</span><span>({
</span><span> namespace: '</span><span style="color:#a3be8c;">api/v1</span><span>'
</span><span>});
</span></code></pre>
<p>On line 3 we change the adapter to be the Rails-style adapter. As we’ve set our API to work the same way as Rails we can make use of its conventions.</p>
<p>On line 4 we prefix all API requests with <code>api/v1</code> which is how we’ve set it in our API.</p>
<h3 id="routes-and-resources">Routes and Resources</h3>
<p>Let’s generate the Post resource, routes and templates. Ember CLI makes this super easy:</p>
<pre style="background-color:#2b303b;color:#c0c5ce;"><code><span>ember g resource posts title:string content:string createdAt:string updatedAt:string
</span><span>ember g route posts/index
</span><span>ember g route posts/new
</span><span>ember g route posts/show
</span><span>ember g route posts/edit
</span></code></pre>
<p>This sets us up with all the files we will need to edit.</p>
<p>Note how <code>createdAt</code> and <code>updatedAt</code> are camel case here but snake case in Padrino. The <code>ActiveModelAdapter</code> will take care of this conversion so we don’t have to!</p>
<p>In <code>app/routes/posts.js</code> we should tell Ember where to find our posts like so:</p>
<pre data-lang="js" style="background-color:#2b303b;color:#c0c5ce;" class="language-js "><code class="language-js" data-lang="js"><span style="color:#b48ead;">import </span><span style="color:#bf616a;">Ember </span><span style="color:#b48ead;">from </span><span>'</span><span style="color:#a3be8c;">ember</span><span>';
</span><span>
</span><span style="color:#b48ead;">export default </span><span style="color:#bf616a;">Ember</span><span>.</span><span style="color:#bf616a;">Route</span><span>.</span><span style="color:#8fa1b3;">extend</span><span>({
</span><span> </span><span style="color:#8fa1b3;">model</span><span>: </span><span style="color:#b48ead;">function</span><span>() {
</span><span> </span><span style="color:#b48ead;">return </span><span style="color:#bf616a;">this</span><span>.</span><span style="color:#bf616a;">store</span><span>.</span><span style="color:#96b5b4;">find</span><span>('</span><span style="color:#a3be8c;">post</span><span>');
</span><span> }
</span><span>});
</span></code></pre>
<h2 id="index">Index</h2>
<p>Let’s update the posts index template in <code>app/templates/posts/index.hbs</code> to iterate over the posts:</p>
<pre data-lang="html" style="background-color:#2b303b;color:#c0c5ce;" class="language-html "><code class="language-html" data-lang="html"><span><</span><span style="color:#bf616a;">h3</span><span>>Latest Posts</</span><span style="color:#bf616a;">h3</span><span>>
</span><span><</span><span style="color:#bf616a;">h3</span><span>>Latest Posts</</span><span style="color:#bf616a;">h3</span><span>>
</span><span><</span><span style="color:#bf616a;">table </span><span style="color:#d08770;">class</span><span>="</span><span style="color:#a3be8c;">table table-striped</span><span>">
</span><span> <</span><span style="color:#bf616a;">tr</span><span>>
</span><span> <</span><span style="color:#bf616a;">th </span><span style="color:#d08770;">class</span><span>=</span><span style="color:#a3be8c;">“col-md-3”</span><span>>Title</</span><span style="color:#bf616a;">th</span><span>>
</span><span> <</span><span style="color:#bf616a;">th </span><span style="color:#d08770;">class</span><span>="</span><span style="color:#a3be8c;">col-md-3</span><span>">Created at</</span><span style="color:#bf616a;">th</span><span>>
</span><span> <</span><span style="color:#bf616a;">th </span><span style="color:#d08770;">class</span><span>="</span><span style="color:#a3be8c;">col-md-3</span><span>">Last modified</</span><span style="color:#bf616a;">th</span><span>>
</span><span> <</span><span style="color:#bf616a;">th </span><span style="color:#d08770;">class</span><span>="</span><span style="color:#a3be8c;">col-md-3</span><span>">Actions</</span><span style="color:#bf616a;">th</span><span>>
</span><span> </</span><span style="color:#bf616a;">tr</span><span>>
</span><span>{{#each post in model}}
</span><span><</span><span style="color:#bf616a;">tr</span><span>>
</span><span> <</span><span style="color:#bf616a;">td</span><span>>{{ post.title }}</</span><span style="color:#bf616a;">td</span><span>>
</span><span> <</span><span style="color:#bf616a;">td</span><span>>{{ post.createdAt }}</</span><span style="color:#bf616a;">td</span><span>>
</span><span> <</span><span style="color:#bf616a;">td</span><span>>{{ post.updatedAt }}</</span><span style="color:#bf616a;">td</span><span>>
</span><span> <</span><span style="color:#bf616a;">td</span><span>>
</span><span> {{#link-to 'posts.show' post classNames="btn btn-success"}}View{{/link-to}}
</span><span> {{#link-to 'posts.edit' post classNames="btn btn-info”}}Edit{{/link-to}}
</span><span> <</span><span style="color:#bf616a;">button </span><span style="color:#d08770;">{{action </span><span style="background-color:#bf616a;color:#2b303b;">'</span><span style="color:#d08770;">delete</span><span style="background-color:#bf616a;color:#2b303b;">'</span><span> </span><span style="color:#d08770;">post}} class</span><span>="</span><span style="color:#a3be8c;">btn btn-danger</span><span>">Delete</</span><span style="color:#bf616a;">button</span><span>>
</span><span> </</span><span style="color:#bf616a;">td</span><span>>
</span><span></</span><span style="color:#bf616a;">tr</span><span>>
</span><span>{{/each}}
</span><span></</span><span style="color:#bf616a;">table</span><span>>
</span><span>{{#link-to 'posts.new' classNames="btn btn-primary"}}
</span><span> New Post
</span><span>{{/link-to}}
</span></code></pre>
<p>Nothing too interesting is going on here. We are looping through our model in a Bootstrap-flavoured table. Each post has 2 links, 1 to view the post and 1 to edit. It also has an action to remove the post. None of these do anything as of yet, so let’s get the rest of the application working.</p>
<h2 id="new">New</h2>
<p>The posts new route file (<code>app/routes/posts/new.js</code>) will have three jobs:</p>
<pre data-lang="js" style="background-color:#2b303b;color:#c0c5ce;" class="language-js "><code class="language-js" data-lang="js"><span style="color:#b48ead;">import </span><span style="color:#bf616a;">Ember </span><span style="color:#b48ead;">from </span><span>'</span><span style="color:#a3be8c;">ember</span><span>';
</span><span>
</span><span style="color:#b48ead;">export default </span><span style="color:#bf616a;">Ember</span><span>.</span><span style="color:#bf616a;">Route</span><span>.</span><span style="color:#8fa1b3;">extend</span><span>({
</span><span> </span><span style="color:#8fa1b3;">model</span><span>: </span><span style="color:#b48ead;">function</span><span>() {
</span><span> </span><span style="color:#b48ead;">return </span><span style="color:#bf616a;">this</span><span>.</span><span style="color:#bf616a;">store</span><span>.</span><span style="color:#8fa1b3;">createRecord</span><span>('</span><span style="color:#a3be8c;">post</span><span>');
</span><span> },
</span><span> </span><span style="color:#8fa1b3;">deactivate</span><span>: </span><span style="color:#b48ead;">function</span><span>() {
</span><span> </span><span style="color:#b48ead;">var </span><span style="color:#bf616a;">model </span><span>= </span><span style="color:#bf616a;">this</span><span>.</span><span style="color:#8fa1b3;">modelFor</span><span>('</span><span style="color:#a3be8c;">posts/new</span><span>');
</span><span>
</span><span> </span><span style="color:#b48ead;">if</span><span>(</span><span style="color:#bf616a;">model</span><span>.</span><span style="color:#96b5b4;">get</span><span>('</span><span style="color:#a3be8c;">isNew</span><span>')) {
</span><span> </span><span style="color:#bf616a;">model</span><span>.</span><span style="color:#8fa1b3;">destroyRecord</span><span>();
</span><span> }
</span><span> },
</span><span> actions: {
</span><span> </span><span style="color:#8fa1b3;">save</span><span>: </span><span style="color:#b48ead;">function</span><span>() {
</span><span> </span><span style="color:#b48ead;">var </span><span style="color:#bf616a;">_this </span><span>= </span><span style="color:#bf616a;">this</span><span>;
</span><span> </span><span style="color:#bf616a;">this</span><span>.</span><span style="color:#8fa1b3;">modelFor</span><span>('</span><span style="color:#a3be8c;">posts/new</span><span>').</span><span style="color:#8fa1b3;">save</span><span>().</span><span style="color:#96b5b4;">then</span><span>(</span><span style="color:#b48ead;">function</span><span>() {
</span><span> </span><span style="color:#bf616a;">_this</span><span>.</span><span style="color:#8fa1b3;">transitionTo</span><span>('</span><span style="color:#a3be8c;">posts.index</span><span>');
</span><span> });
</span><span> }
</span><span> }
</span><span>});
</span></code></pre>
<p>The model method will create a new instance of post for us in the Ember application.</p>
<p>Deactivate will trigger if we switch out of the route without saving. We use this because if we don’t save the the new post to the API (we click cancel) Ember will keep this “dirty” object hanging around in the application. <code>destroyRecord</code> will remove it for us.</p>
<p>Lastly we set the save action to create the post and go back to the posts.index route.</p>
<p>Modify two templates, <code>app/templates/posts/new.hbs</code></p>
<pre data-lang="html" style="background-color:#2b303b;color:#c0c5ce;" class="language-html "><code class="language-html" data-lang="html"><span><</span><span style="color:#bf616a;">h3</span><span>>New Post</</span><span style="color:#bf616a;">h3</span><span>>
</span><span>{{ partial 'posts/form' }}
</span></code></pre>
<p>Our Form partial located in <code>app/templates/posts/-form.hbs</code></p>
<pre data-lang="html" style="background-color:#2b303b;color:#c0c5ce;" class="language-html "><code class="language-html" data-lang="html"><span><</span><span style="color:#bf616a;">form </span><span style="color:#d08770;">{{action </span><span style="background-color:#bf616a;color:#2b303b;">"</span><span style="color:#d08770;">save</span><span style="background-color:#bf616a;color:#2b303b;">"</span><span> </span><span style="color:#d08770;">on</span><span>="</span><span style="color:#a3be8c;">submit</span><span>"</span><span style="color:#d08770;">}}</span><span>>
</span><span> <</span><span style="color:#bf616a;">div </span><span style="color:#d08770;">class</span><span>="</span><span style="color:#a3be8c;">form-group</span><span>">
</span><span> <</span><span style="color:#bf616a;">label </span><span style="color:#d08770;">for</span><span>="</span><span style="color:#a3be8c;">title</span><span>">Title</</span><span style="color:#bf616a;">label</span><span>>
</span><span> {{ input value=model.title class="form-control"}}
</span><span> </</span><span style="color:#bf616a;">div</span><span>>
</span><span>
</span><span> <</span><span style="color:#bf616a;">div </span><span style="color:#d08770;">class</span><span>="</span><span style="color:#a3be8c;">form-group</span><span>">
</span><span> <</span><span style="color:#bf616a;">label </span><span style="color:#d08770;">for</span><span>="</span><span style="color:#a3be8c;">content</span><span>">Content</</span><span style="color:#bf616a;">label</span><span>>
</span><span> {{ textarea value=model.content class="form-control"}}
</span><span> </</span><span style="color:#bf616a;">div</span><span>>
</span><span>
</span><span> <</span><span style="color:#bf616a;">button </span><span style="color:#d08770;">type</span><span>="</span><span style="color:#a3be8c;">submit</span><span>" </span><span style="color:#d08770;">class</span><span>="</span><span style="color:#a3be8c;">btn btn-primary</span><span>">Save</</span><span style="color:#bf616a;">button</span><span>>
</span><span> {{#link-to 'posts.index'}}<</span><span style="color:#bf616a;">button </span><span style="color:#d08770;">class</span><span>="</span><span style="color:#a3be8c;">btn btn-default</span><span>">Cancel</</span><span style="color:#bf616a;">button</span><span>>{{/link-to}}
</span><span></</span><span style="color:#bf616a;">form</span><span>>
</span></code></pre>
<p>This partial template will be used for our edit form as well.</p>
<p>Here’s the how the form looks now:</p>
<p><a href="https://i.imgur.com/5ieTDRa.png"><img src="https://i.imgur.com/5ieTDRa.png" alt="New Post" /></a></p>
<h2 id="edit">Edit</h2>
<p><code>app/routes/posts/new.js</code> will handle saving and rolling back if a user decides to cancel an edit:</p>
<pre data-lang="js" style="background-color:#2b303b;color:#c0c5ce;" class="language-js "><code class="language-js" data-lang="js"><span style="color:#b48ead;">import </span><span style="color:#bf616a;">Ember </span><span style="color:#b48ead;">from </span><span>'</span><span style="color:#a3be8c;">ember</span><span>';
</span><span>
</span><span style="color:#b48ead;">export default </span><span style="color:#bf616a;">Ember</span><span>.</span><span style="color:#bf616a;">Route</span><span>.</span><span style="color:#8fa1b3;">extend</span><span>({
</span><span> </span><span style="color:#8fa1b3;">deactivate</span><span>: </span><span style="color:#b48ead;">function</span><span>() {
</span><span> </span><span style="color:#b48ead;">var </span><span style="color:#bf616a;">model </span><span>= </span><span style="color:#bf616a;">this</span><span>.</span><span style="color:#8fa1b3;">modelFor</span><span>('</span><span style="color:#a3be8c;">posts/edit</span><span>');
</span><span> </span><span style="color:#bf616a;">model</span><span>.</span><span style="color:#8fa1b3;">rollback</span><span>();
</span><span> },
</span><span> actions: {
</span><span> </span><span style="color:#8fa1b3;">save</span><span>: </span><span style="color:#b48ead;">function</span><span>() {
</span><span> </span><span style="color:#b48ead;">var </span><span style="color:#bf616a;">_this </span><span>= </span><span style="color:#bf616a;">this</span><span>;
</span><span> </span><span style="color:#bf616a;">this</span><span>.</span><span style="color:#8fa1b3;">modelFor</span><span>('</span><span style="color:#a3be8c;">posts/edit</span><span>').</span><span style="color:#8fa1b3;">save</span><span>().</span><span style="color:#96b5b4;">then</span><span>(</span><span style="color:#b48ead;">function</span><span>() {
</span><span> </span><span style="color:#bf616a;">_this</span><span>.</span><span style="color:#8fa1b3;">transitionTo</span><span>('</span><span style="color:#a3be8c;">posts.index</span><span>');
</span><span> });
</span><span> }
</span><span> }
</span><span>});
</span></code></pre>
<p>And because we made our form a partial that is only tied to the existence of a model our <code>app/templates/posts/edit.js</code> is as simple as this</p>
<pre data-lang="html" style="background-color:#2b303b;color:#c0c5ce;" class="language-html "><code class="language-html" data-lang="html"><span><</span><span style="color:#bf616a;">h3</span><span>>Edit Post: {{model.title}}</</span><span style="color:#bf616a;">h3</span><span>>
</span><span>{{ partial 'posts/form'}}
</span></code></pre>
<p><a href="https://i.imgur.com/1U1nIMU.png"><img src="https://i.imgur.com/1U1nIMU.png" alt="Edit Post" /></a></p>
<h2 id="show">Show</h2>
<p>For when we want to view the post without editing. Thanks to Ember having awesome conventions we only need to edit the template file <code>app/templates/posts/show.hbs</code></p>
<pre data-lang="html" style="background-color:#2b303b;color:#c0c5ce;" class="language-html "><code class="language-html" data-lang="html"><span><</span><span style="color:#bf616a;">h3</span><span>>{{model.title}}</</span><span style="color:#bf616a;">h3</span><span>>
</span><span><</span><span style="color:#bf616a;">div</span><span>>{{model.content}}</</span><span style="color:#bf616a;">div</span><span>>
</span><span>{{#link-to 'posts.index' classNames="btn btn-default"}}Back{{/link-to}}
</span></code></pre>
<h2 id="delete">Delete</h2>
<p>Last but by no means least we need to place a delete action in our <code>app/routes/posts/index.js</code>. How else would we delete all the test posts we’ve made?</p>
<pre data-lang="js" style="background-color:#2b303b;color:#c0c5ce;" class="language-js "><code class="language-js" data-lang="js"><span style="color:#b48ead;">import </span><span style="color:#bf616a;">Ember </span><span style="color:#b48ead;">from </span><span>'</span><span style="color:#a3be8c;">ember</span><span>';
</span><span>
</span><span style="color:#b48ead;">export default </span><span style="color:#bf616a;">Ember</span><span>.</span><span style="color:#bf616a;">Route</span><span>.</span><span style="color:#8fa1b3;">extend</span><span>({
</span><span> actions: {
</span><span> </span><span style="color:#8fa1b3;">delete</span><span>: </span><span style="color:#b48ead;">function</span><span>(</span><span style="color:#bf616a;">post</span><span>) {
</span><span> </span><span style="color:#b48ead;">var </span><span style="color:#bf616a;">_this </span><span>= </span><span style="color:#bf616a;">this</span><span>;
</span><span>
</span><span> </span><span style="color:#bf616a;">post</span><span>.</span><span style="color:#8fa1b3;">destroyRecord</span><span>().</span><span style="color:#96b5b4;">then</span><span>(</span><span style="color:#b48ead;">function</span><span>() {
</span><span> </span><span style="color:#bf616a;">_this</span><span>.</span><span style="color:#8fa1b3;">transitionTo</span><span>('</span><span style="color:#a3be8c;">posts.index</span><span>');
</span><span> });
</span><span>
</span><span> }
</span><span> }
</span><span>});
</span></code></pre>
<p>Here’s a preview of our posts index with a couple of posts added:</p>
<p><a href="https://i.imgur.com/7xiLC8c.png"><img src="https://i.imgur.com/7xiLC8c.png" alt="Posts" /></a></p>
<h2 id="what-next">What next?</h2>
<p>If you're interested in setting up authentication you can check part 3 of this series, <a href="/articles/using-padrino-with-ember-cli-part-3-authentication">Using Padrino with Ember: Authentication</a>.</p>
<p>There is a lot of room for growth. Things that are often needed in SPA’s include validation, flash messages and realtime data. </p>
<p>They are all very doable with Padrino and Ember but are beyond the scope of this article.</p>
<p>For server-side validation take a look at the <a href="http://sequel.jeremyevans.net/rdoc/files/doc/validations_rdoc.html">Sequel validation documentation</a>. Infact, the whole Sequel documentation is pretty awesome.</p>
<p>I recommend adding an error attribute to RABL objects which Ember can check for. If it exists it should stop transitions and loop through all of the errors in the template.</p>
<p>For flash messages I tend to create a mixin from the code in <a href="http://aaronvb.com/articles/ember-js-flash-messages.html">this article</a> which works well.</p>
<p>For realtime data I’ve had success with <a href="https://pusher.com/">Pusher</a> which has a nice free package. The <a href="https://github.com/pusher/pusher-gem">Pusher gem</a> is really easy to integrate with Padrino.</p>
<h2 id="summary">Summary</h2>
<p>Well there you have it. Ember CLI with a Padrino backend. All the beauty of Ruby without the all of the unnecessary parts of Rails.</p>
<p>In these two posts we’ve covered a lot of the core functionality needed in any single page application. Creating, reading, updating and deleting data from our API and from Ember.</p>
<p>We’ve also taken a look at what we can use when we need to do more than just simple CRUD commands.</p>
<p>If you wish to see the full source for both of these articles you can view it on <a href="https://github.com/acoustep/padrino-ember-example">Github</a>.</p>
Using Padrino with Ember CLI Part 12015-02-11T00:00:00+00:002015-02-11T00:00:00+00:00https://www.fullstackstanley.com/articles/using-padrino-with-ember-cli-part-1/<p>After listening to the Ruby Rogues’ Padrino episode I was sold on the idea of using Padrino for smaller websites and simple API’s. I know <a href="https://github.com/intridea/grape">Grape</a> is also a perfect contender for building an Ember APIs but:</p>
<span id="continue-reading"></span>
<ul>
<li>I am already familiar with Sinatra so the learning curve shouldn’t be as steep.</li>
<li>I wanted to get to know Padrino.</li>
<li>Funsies.
I’m going to show you how to quickly set up both together, build a restful API compatible with Ember’s <code>ActiveModelAdapter</code> and show you a few gotchas to help you on your way.</li>
</ul>
<h2 id="note">Note</h2>
<p>This article is aimed at people familiar with Ruby and have some understanding of the MVC pattern (ideally with Padrino or Rails). This article is not a guide for starting out with Padrino. If you have previous Ruby web framework experience you’ll most likely catch on quick.</p>
<p>On the Ember side of things you should be comfortable with Javascript, Ember and ideally Ember-CLI.</p>
<p>I will try to explain everything I do but this article is more about getting the two frameworks to work together.</p>
<p>You can view the full source for both of these articles on <a href="https://github.com/acoustep/padrino-ember-example">Github</a>.</p>
<h2 id="getting-started">Getting Started</h2>
<p>Make sure you have Padrino and Ember-CLI installed. With Ruby and NPM this should be a matter of <code>gem install pardrino</code>, <code>npm install -g ember-cli</code> and <code>npm install -g bower</code>.
See Padrino’s <a href="http://www.padrinorb.com/guides/installation">Installation Guide</a> and Ember CLI’s <a href="http://www.ember-cli.com/#getting-started">Getting Started</a> for more details.
For reference I’m using the following library versions:</p>
<ul>
<li>IO.js 1.2.0 (Also tested with Node 0.10.36)</li>
<li>NPM 2.5.1</li>
<li>Ember-cli 0.1.15</li>
<li>Ember 1.8.1</li>
<li>Ember-data 1.0.0-beta.14.1</li>
<li>Bower 1.3.12</li>
<li>ruby-2.1.1</li>
<li>Padrino 0.12.4</li>
</ul>
<p>For a directory structure to this article we want 3 directories.
</p>
<pre style="background-color:#2b303b;color:#c0c5ce;"><code><span> - Blog
</span><span> |- API
</span><span> |- App
</span></code></pre>
<p>Make <code>Blog</code> main directory and change in to it.</p>
<pre style="background-color:#2b303b;color:#c0c5ce;"><code><span>mkdir Blog && cd Blog
</span></code></pre>
<h2 id="setting-up-padrino">Setting up Padrino</h2>
<p>One of the features of Padrino I love the most is the ability to swap different libraries to your preference. For instance, you have the choice to use your preferred ORM. If you know Rails then you will most likely be comfortable with ActiveRecord. </p>
<p>I have recently been dabbling with Sequel and will be using it for this tutorial as I have become quite fond of it.</p>
<p>Run the following commands to set up Padrino</p>
<pre style="background-color:#2b303b;color:#c0c5ce;"><code><span>padrino g project API -d sequel -t minitest
</span><span>
</span><span>cd ./API
</span><span>bundle
</span></code></pre>
<p>Check the <a href="http://www.padrinorb.com/guides/generators">Generators</a> documentation for more options. You can specify css precompilers, Javascript libraries and mocking libraries as well. As they are not really relevant to this article I have left them off.</p>
<p>Open up your Gemfile in <code>API/Gemfile</code> and add the following gems:</p>
<pre data-lang="ruby" style="background-color:#2b303b;color:#c0c5ce;" class="language-ruby "><code class="language-ruby" data-lang="ruby"><span style="color:#8fa1b3;">gem </span><span>'</span><span style="color:#a3be8c;">rabl</span><span>'
</span><span style="color:#8fa1b3;">gem </span><span>'</span><span style="color:#a3be8c;">oj</span><span>'
</span><span style="color:#8fa1b3;">gem </span><span>'</span><span style="color:#a3be8c;">rack-cors</span><span>'</span><span style="color:#8fa1b3;">, </span><span style="color:#a3be8c;">:require </span><span style="color:#8fa1b3;">=> </span><span>'</span><span style="color:#a3be8c;">rack/cors</span><span>'
</span><span>
</span><span style="color:#65737e;"># Optional
</span><span>group </span><span style="color:#a3be8c;">:development </span><span style="color:#b48ead;">do
</span><span> </span><span style="color:#8fa1b3;">gem </span><span>"</span><span style="color:#a3be8c;">better_errors</span><span>"
</span><span style="color:#b48ead;">end
</span></code></pre>
<p><a href="https://github.com/nesquena/rabl">RABL</a> stands for “Ruby API Builder language” and let’s us generate JSON for our API incredibly easily. I have to say, I found the documentation incredibly useful and I have found examples for every use case I’ve needed so far.</p>
<p><a href="https://github.com/ohler55/oj">Oj</a> is a RABL dependancy for “speed optimized JSON handling”.</p>
<p><a href="https://github.com/cyu/rack-cors">Rack/CORS</a> “provides support for Cross-Origin Resource Sharing (CORS) for Rack compatible web applications.”</p>
<p><a href="https://github.com/charliesome/better_errors">Better Errors</a> makes debugging so much easier. You can remove it for the tutorial but I do recommend using it in your own applications.</p>
<p>Run <code>bundle</code> to install the new dependancies.</p>
<p>For the sake of keeping the article short we will be creating the API in one Padrino application but it is possible to mount multiple Padrino apps within one another. </p>
<p>We could have one app mounted to <code>api/v1/</code> and then, if we plan to upgrade our API, it’s as simple as mounting another app to <code>api/v2/</code>. </p>
<p>Take a look at the <a href="http://www.padrinorb.com/guides/mounting-applications">Mounting Applications</a> documentation for more information on mounting apps. Also check <a href="http://www.padrinorb.com/guides/generators#sub-app-generator">here</a> for generating the sub-applications.</p>
<h2 id="the-model">The Model</h2>
<p>Keeping things simple, we’ll have one CRUD API for managing blog posts using using the default sqlite database.</p>
<p>We’ll make the model called <code>Post</code> that has 5 fields, <code>id</code>, <code>title</code>, <code>content</code>, <code>created_at</code> and <code>updated_at</code></p>
<pre style="background-color:#2b303b;color:#c0c5ce;"><code><span>padrino g model Post title:string content:text created_at:datetime updated_at:datetime
</span></code></pre>
<p>This will create 3 files, your model, a migration file and a file for your model tests.</p>
<p>Open the migration file located in <code>db/migrate/001_create_posts.rb</code> and change the updated_at field to be nullable.</p>
<pre data-lang="ruby" style="background-color:#2b303b;color:#c0c5ce;" class="language-ruby "><code class="language-ruby" data-lang="ruby"><span style="color:#ebcb8b;">Sequel</span><span>.migration </span><span style="color:#b48ead;">do
</span><span> up </span><span style="color:#b48ead;">do
</span><span> create_table </span><span style="color:#a3be8c;">:posts </span><span style="color:#b48ead;">do
</span><span> primary_key </span><span style="color:#a3be8c;">:id
</span><span> </span><span style="color:#96b5b4;">String </span><span style="color:#a3be8c;">:title
</span><span> </span><span style="color:#bf616a;">Text </span><span style="color:#a3be8c;">:content
</span><span> </span><span style="color:#bf616a;">DateTime </span><span style="color:#a3be8c;">:created_at
</span><span> </span><span style="color:#bf616a;">DateTime </span><span style="color:#a3be8c;">:updated_at</span><span>, </span><span style="color:#a3be8c;">null:</span><span style="color:#d08770;">true
</span><span> </span><span style="color:#b48ead;">end
</span><span> </span><span style="color:#b48ead;">end
</span><span>
</span><span> down </span><span style="color:#b48ead;">do
</span><span> drop_table </span><span style="color:#a3be8c;">:posts
</span><span> </span><span style="color:#b48ead;">end
</span><span style="color:#b48ead;">end
</span></code></pre>
<p>When using Sequel you can migrate your database to the latest migration with this rake command:</p>
<pre style="background-color:#2b303b;color:#c0c5ce;"><code><span>rake sq:migrate:up
</span></code></pre>
<p>To make our timestamp columns behave like Ruby on Rails we need to add the Sequel timestamp plugin in our <code>config/database.rb</code>.</p>
<pre data-lang="ruby" style="background-color:#2b303b;color:#c0c5ce;" class="language-ruby "><code class="language-ruby" data-lang="ruby"><span style="color:#ebcb8b;">Sequel</span><span>::</span><span style="color:#ebcb8b;">Model</span><span>.plugin(</span><span style="color:#a3be8c;">:timestamps</span><span>)
</span></code></pre>
<p>In the <code>models/post.rb</code> add the following line:</p>
<pre data-lang="ruby" style="background-color:#2b303b;color:#c0c5ce;" class="language-ruby "><code class="language-ruby" data-lang="ruby"><span style="color:#b48ead;">class </span><span style="color:#ebcb8b;">Post </span><span style="color:#eff1f5;">< </span><span style="color:#a3be8c;">Sequel::Model
</span><span> </span><span style="color:#ebcb8b;">Sequel</span><span>::</span><span style="color:#ebcb8b;">Model</span><span>.plugin </span><span style="color:#a3be8c;">:timestamps
</span><span style="color:#b48ead;">end
</span></code></pre>
<p>This line will sort out updating <code>created_at</code> and <code>updated_at</code> when necessary.</p>
<h2 id="the-controller">The Controller</h2>
<p>Run the following to generate the controller:</p>
<pre style="background-color:#2b303b;color:#c0c5ce;"><code><span>padrino g controller Posts get:index post:create get:show patch:update delete:destroy
</span></code></pre>
<p>This will create our controller in <code>app/controllers/post.rb</code>. We can see in the command we’ve deliberately left out new and edit methods from our CRUD API. This is because Ember has no need to query them (at least it won’t for our simple application).</p>
<p>Running <code>rake routes</code> we’ll see how we can interact with the routes.</p>
<pre style="background-color:#2b303b;color:#c0c5ce;"><code><span> URL REQUEST PATH
</span><span> (:posts, :index) GET /posts
</span><span> (:posts, :create) POST /posts/create
</span><span> (:posts, :show) GET /posts/show
</span><span> (:posts, :update) PATCH /posts/update
</span><span> (:posts, :destroy) DELETE /posts/destroy
</span></code></pre>
<p>Let’s make it so api/v1 is prepended and for familiarity we’ll also modify our routes to mirror Rail’s conventions found <a href="http://guides.rubyonrails.org/routing.html#crud-verbs-and-actions">here</a>.</p>
<p>In <code>app/controllers/post.rb</code> change it to the following:</p>
<p><strong>Edit:</strong> Big thanks to <a href="https://twitter.com/nesquena">Nathan Esquenazi</a> for showing me a much cleaner way of doing it. For reference you can see my previous code <a href="https://github.com/acoustep/padrino-ember-example/blob/1a0cadc73e4532cca781f2655106f4d878662575/API/app/controllers/posts.rb">here</a></p>
<pre data-lang="ruby" style="background-color:#2b303b;color:#c0c5ce;" class="language-ruby "><code class="language-ruby" data-lang="ruby"><span style="color:#ebcb8b;">Api</span><span>::</span><span style="color:#ebcb8b;">App</span><span>.controllers </span><span style="color:#a3be8c;">:posts</span><span>, </span><span style="color:#a3be8c;">map: </span><span>"</span><span style="color:#a3be8c;">api/v1/posts</span><span>" </span><span style="color:#b48ead;">do
</span><span>
</span><span> get </span><span style="color:#a3be8c;">:index</span><span>, </span><span style="color:#a3be8c;">map: </span><span>"" </span><span style="color:#b48ead;">do
</span><span>
</span><span> </span><span style="color:#b48ead;">end
</span><span>
</span><span> post </span><span style="color:#a3be8c;">:create</span><span>, </span><span style="color:#a3be8c;">map: </span><span>"" </span><span style="color:#b48ead;">do
</span><span>
</span><span> </span><span style="color:#b48ead;">end
</span><span>
</span><span> get </span><span style="color:#a3be8c;">:show</span><span>, </span><span style="color:#a3be8c;">map: </span><span>"</span><span style="color:#a3be8c;">:id</span><span>" </span><span style="color:#b48ead;">do
</span><span>
</span><span> </span><span style="color:#b48ead;">end
</span><span>
</span><span> patch </span><span style="color:#a3be8c;">:update</span><span>, </span><span style="color:#a3be8c;">map: </span><span>"</span><span style="color:#a3be8c;">:id</span><span>" </span><span style="color:#b48ead;">do
</span><span>
</span><span> </span><span style="color:#b48ead;">end
</span><span>
</span><span> delete </span><span style="color:#a3be8c;">:destroy</span><span>, </span><span style="color:#a3be8c;">map: </span><span>"</span><span style="color:#a3be8c;">:id</span><span>" </span><span style="color:#b48ead;">do
</span><span>
</span><span> </span><span style="color:#b48ead;">end
</span><span>
</span><span style="color:#b48ead;">end
</span></code></pre>
<p>If you run <code>rake routes</code> now you should see the following:</p>
<pre style="background-color:#2b303b;color:#c0c5ce;"><code><span> (:posts, :index) GET /api/v1/posts
</span><span> (:posts, :create) POST /api/v1/posts
</span><span> (:posts, :show) GET /api/v1/posts/:id
</span><span> (:posts, :update) PATCH /api/v1/posts/:id
</span><span> (:posts, :destroy) DELETE /api/v1/posts/:id
</span></code></pre>
<p>Much better. Now let’s fill in the details</p>
<pre data-lang="ruby" style="background-color:#2b303b;color:#c0c5ce;" class="language-ruby "><code class="language-ruby" data-lang="ruby"><span style="color:#8fa1b3;">require </span><span>'</span><span style="color:#a3be8c;">json</span><span>'
</span><span style="color:#ebcb8b;">Api</span><span>::</span><span style="color:#ebcb8b;">App</span><span>.controllers </span><span style="color:#a3be8c;">:posts</span><span>, </span><span style="color:#a3be8c;">map: </span><span>"</span><span style="color:#a3be8c;">api/v1/posts</span><span>" </span><span style="color:#b48ead;">do
</span><span>
</span><span> get </span><span style="color:#a3be8c;">:index</span><span>, </span><span style="color:#a3be8c;">map: </span><span>"" </span><span style="color:#b48ead;">do
</span><span> @</span><span style="color:#bf616a;">posts </span><span>= </span><span style="color:#ebcb8b;">Post</span><span>.all
</span><span> render "</span><span style="color:#a3be8c;">posts/index</span><span>"
</span><span> </span><span style="color:#b48ead;">end
</span><span>
</span><span> post </span><span style="color:#a3be8c;">:create</span><span>, </span><span style="color:#a3be8c;">map: </span><span>"" </span><span style="color:#b48ead;">do
</span><span> parameters = post_params
</span><span> </span><span style="color:#b48ead;">if</span><span> parameters["</span><span style="color:#a3be8c;">post</span><span>"].</span><span style="color:#96b5b4;">nil?
</span><span> </span><span style="color:#b48ead;">return </span><span>'</span><span style="color:#a3be8c;">{}</span><span>'
</span><span> </span><span style="color:#b48ead;">end
</span><span> @</span><span style="color:#bf616a;">post </span><span>= </span><span style="color:#ebcb8b;">Post</span><span>.create parameters["</span><span style="color:#a3be8c;">post</span><span>"]
</span><span> render "</span><span style="color:#a3be8c;">posts/show</span><span>"
</span><span> </span><span style="color:#b48ead;">end
</span><span>
</span><span> get </span><span style="color:#a3be8c;">:show</span><span>, </span><span style="color:#a3be8c;">map: </span><span>"</span><span style="color:#a3be8c;">:id</span><span>" </span><span style="color:#b48ead;">do
</span><span> @</span><span style="color:#bf616a;">post </span><span>= </span><span style="color:#ebcb8b;">Post</span><span>[params[</span><span style="color:#a3be8c;">:id</span><span>]]
</span><span> render "</span><span style="color:#a3be8c;">posts/show</span><span>"
</span><span> </span><span style="color:#b48ead;">end
</span><span>
</span><span> put </span><span style="color:#a3be8c;">:update</span><span>, </span><span style="color:#a3be8c;">map: </span><span>"</span><span style="color:#a3be8c;">:id</span><span>" </span><span style="color:#b48ead;">do
</span><span> @</span><span style="color:#bf616a;">post </span><span>= </span><span style="color:#ebcb8b;">Post</span><span>[params[</span><span style="color:#a3be8c;">:id</span><span>]]
</span><span>
</span><span> </span><span style="color:#b48ead;">if </span><span>@</span><span style="color:#bf616a;">post</span><span>.</span><span style="color:#96b5b4;">nil?
</span><span> </span><span style="color:#b48ead;">return </span><span>'</span><span style="color:#a3be8c;">{}</span><span>'
</span><span> </span><span style="color:#b48ead;">end
</span><span>
</span><span> parameters = post_params
</span><span> @</span><span style="color:#bf616a;">post</span><span>.update parameters["</span><span style="color:#a3be8c;">post</span><span>"]
</span><span> render "</span><span style="color:#a3be8c;">posts/show</span><span>"
</span><span>
</span><span> </span><span style="color:#b48ead;">end
</span><span>
</span><span> delete </span><span style="color:#a3be8c;">:destroy</span><span>, </span><span style="color:#a3be8c;">map: </span><span>"</span><span style="color:#a3be8c;">:id</span><span>" </span><span style="color:#b48ead;">do
</span><span> @</span><span style="color:#bf616a;">post </span><span>= </span><span style="color:#ebcb8b;">Post</span><span>[params[</span><span style="color:#a3be8c;">:id</span><span>]]
</span><span> @</span><span style="color:#bf616a;">post</span><span>.delete </span><span style="color:#b48ead;">unless </span><span>@</span><span style="color:#bf616a;">post</span><span>.</span><span style="color:#96b5b4;">nil?
</span><span> </span><span style="color:#b48ead;">end
</span><span>
</span><span style="color:#b48ead;">end
</span><span>
</span><span style="color:#b48ead;">def </span><span style="color:#8fa1b3;">post_params
</span><span> </span><span style="color:#ebcb8b;">JSON</span><span>.parse(request.body.read)
</span><span style="color:#b48ead;">end
</span></code></pre>
<p>If you're familiar with Active Record you should see some familiar method names. <code>all</code>, <code>create</code>, <code>delete</code> and <code>update</code> are self explanatory. You may be unfamiliar with how Sequel finds specific rows, though.</p>
<p><code>Post[params[:id]]</code> is the simplest way to retrieve a record by primary key. <code>params[:id]</code> is just the URL id parameter. So it's more like calling <code>Post[1]</code>.</p>
<p>Occasionally we return blank objects with </p>
<pre style="background-color:#2b303b;color:#c0c5ce;"><code><span>return '{}'
</span></code></pre>
<p>This may seem unneeded but Ember expects a valid JSON response and if it doesn't get one it will throw a hissy fit.</p>
<h3 id="views">Views</h3>
<p>Now we’ll get the JSON responses working. Sequel and RABL make this nice and easy.</p>
<p>Create these two files:</p>
<h3 id="app-views-index-rabl">app/views/index.rabl</h3>
<pre data-lang="ruby" style="background-color:#2b303b;color:#c0c5ce;" class="language-ruby "><code class="language-ruby" data-lang="ruby"><span>collection @</span><span style="color:#bf616a;">posts</span><span>, </span><span style="color:#a3be8c;">root: </span><span>"</span><span style="color:#a3be8c;">posts</span><span>", </span><span style="color:#a3be8c;">object_root: </span><span style="color:#d08770;">false
</span><span>attributes </span><span style="color:#a3be8c;">:id</span><span>, </span><span style="color:#a3be8c;">:title</span><span>, </span><span style="color:#a3be8c;">:content</span><span>, </span><span style="color:#a3be8c;">:created_at</span><span>, </span><span style="color:#a3be8c;">:updated_at
</span></code></pre>
<h3 id="app-views-show-rabl">app/views/show.rabl</h3>
<pre data-lang="ruby" style="background-color:#2b303b;color:#c0c5ce;" class="language-ruby "><code class="language-ruby" data-lang="ruby"><span>object @</span><span style="color:#bf616a;">post
</span><span>attributes </span><span style="color:#a3be8c;">:id</span><span>, </span><span style="color:#a3be8c;">:title</span><span>, </span><span style="color:#a3be8c;">:content</span><span>, </span><span style="color:#a3be8c;">:created_at</span><span>, </span><span style="color:#a3be8c;">:updated_at
</span></code></pre>
<p>If you’ve never used RABL before this may look quite alien to you.</p>
<p>We use the <code>collection</code> method when working with multiple objects and the <code>object</code> method when specifying one object in particular.</p>
<p>The collection method has two parameters, <code>root</code> and <code>object_root</code>. <code>root</code> specifies the parent key that wraps around the collection. We set <code>object_root</code> to false because by default RABL adds another key around each row.</p>
<p>Essentially we are changing this JSON:</p>
<pre data-lang="json" style="background-color:#2b303b;color:#c0c5ce;" class="language-json "><code class="language-json" data-lang="json"><span>[{
</span><span> "</span><span style="color:#a3be8c;">post</span><span>":
</span><span> {
</span><span> "</span><span style="color:#a3be8c;">id</span><span>":</span><span style="color:#d08770;">1</span><span>,
</span><span> "</span><span style="color:#a3be8c;">title</span><span>":"</span><span style="color:#a3be8c;">test</span><span>",
</span><span> "</span><span style="color:#a3be8c;">content</span><span>":"</span><span style="color:#a3be8c;">test</span><span>",
</span><span> "</span><span style="color:#a3be8c;">created_at</span><span>":"</span><span style="color:#a3be8c;">2015-02-11 20:25:21 +0000</span><span>",
</span><span> "</span><span style="color:#a3be8c;">updated_at</span><span>":</span><span style="color:#d08770;">null
</span><span> }
</span><span>}]
</span></code></pre>
<p>To this Ember-friendly JSON</p>
<pre data-lang="json" style="background-color:#2b303b;color:#c0c5ce;" class="language-json "><code class="language-json" data-lang="json"><span>{
</span><span> "</span><span style="color:#a3be8c;">posts</span><span>":
</span><span> [{
</span><span> "</span><span style="color:#a3be8c;">id”:1,</span><span style="background-color:#bf616a;color:#2b303b;">
</span><span> </span><span style="background-color:#bf616a;color:#2b303b;">”title</span><span>"</span><span style="color:#a3be8c;">:</span><span>"</span><span style="background-color:#bf616a;color:#2b303b;">test</span><span>"</span><span style="color:#a3be8c;">,</span><span style="background-color:#bf616a;color:#2b303b;">
</span><span> "</span><span style="color:#a3be8c;">content</span><span>":"</span><span style="color:#a3be8c;">test</span><span>",
</span><span> "</span><span style="color:#a3be8c;">created_at</span><span>":"</span><span style="color:#a3be8c;">2015-02-11 20:25:21 +0000</span><span>",
</span><span> "</span><span style="color:#a3be8c;">updated_at</span><span>":</span><span style="color:#d08770;">null
</span><span> }]
</span><span>}
</span></code></pre>
<p>Lastly, the <code>attributes</code> method lets us choose which data we want to show in our API.</p>
<h3 id="a-word-on-csrf-protection">A word on CSRF Protection</h3>
<h4 id="the-problem">The problem</h4>
<p>With Rails and Ember-Rails it’s possible to set up CSRF protection by having an API end-point that provides the CSRF token. That token is then placed in a meta tag called <code>csrf_token</code> and then with Ember you set up an AJAX preFilter which sends the token in a header named <code>X-CSRF-Token</code>.</p>
<p>All this is possible with Padrino’s <code>csrf_token</code> method, but due to the fact that your API and Ember-CLI are separate applications you will not have access to the session which verifies the token. </p>
<h4 id="the-solution">The solution</h4>
<p>You have 2 options. Disable CSRF entirely or, if your Padrino app is more than just an API, disable it for your API.</p>
<h4 id="disabling-csrf-entirely">Disabling CSRF Entirely</h4>
<p>Go to <code>app/config/apps.rb</code> and set the following</p>
<pre data-lang="ruby" style="background-color:#2b303b;color:#c0c5ce;" class="language-ruby "><code class="language-ruby" data-lang="ruby"><span>set </span><span style="color:#a3be8c;">:protect_from_csrf</span><span>, </span><span style="color:#d08770;">false
</span></code></pre>
<p>If your app is currently running make sure you restart it now with <code>bundle exec padrino start</code>.</p>
<h4 id="disabling-csrf-for-your-api">Disabling CSRF for your API</h4>
<p>If you’re using Better Errors, go to <code>app/config/apps.rb</code> and set the following</p>
<pre data-lang="ruby" style="background-color:#2b303b;color:#c0c5ce;" class="language-ruby "><code class="language-ruby" data-lang="ruby"><span>set </span><span style="color:#a3be8c;">:protect_from_csrf</span><span>, </span><span style="color:#a3be8c;">except: </span><span>%r{</span><span style="color:#96b5b4;">/__better_errors/\\w+/\\w+\\z</span><span>} </span><span style="color:#b48ead;">if </span><span style="color:#ebcb8b;">Padrino</span><span>.env == </span><span style="color:#a3be8c;">:development
</span></code></pre>
<p>If your app is currently running make sure you restart it now with <code>bundle exec padrino start</code>.</p>
<p>In <code>app/controllers/posts.rb</code> we need to disable CSRF for any requests that aren’t <code>get</code>. We can do that by setting <code>csrf_protection: false</code> in the parameters.</p>
<pre data-lang="ruby" style="background-color:#2b303b;color:#c0c5ce;" class="language-ruby "><code class="language-ruby" data-lang="ruby"><span style="color:#8fa1b3;">require </span><span>'</span><span style="color:#a3be8c;">json</span><span>'
</span><span style="color:#ebcb8b;">Api</span><span>::</span><span style="color:#ebcb8b;">App</span><span>.controllers </span><span style="color:#a3be8c;">:posts</span><span>, </span><span style="color:#a3be8c;">map: </span><span>"</span><span style="color:#a3be8c;">api/v1/posts</span><span>" </span><span style="color:#b48ead;">do
</span><span>
</span><span> get </span><span style="color:#a3be8c;">:index</span><span>, </span><span style="color:#a3be8c;">map: </span><span>"" </span><span style="color:#b48ead;">do
</span><span> @</span><span style="color:#bf616a;">posts </span><span>= </span><span style="color:#ebcb8b;">Post</span><span>.all
</span><span> render "</span><span style="color:#a3be8c;">posts/index</span><span>"
</span><span> </span><span style="color:#b48ead;">end
</span><span>
</span><span> post </span><span style="color:#a3be8c;">:create</span><span>, </span><span style="color:#a3be8c;">map: </span><span>"", </span><span style="color:#a3be8c;">csrf_protection: </span><span style="color:#d08770;">false </span><span style="color:#b48ead;">do
</span><span> parameters = </span><span style="color:#ebcb8b;">JSON</span><span>.parse(request.body.read)
</span><span> </span><span style="color:#b48ead;">if</span><span> parameters["</span><span style="color:#a3be8c;">post</span><span>"].</span><span style="color:#96b5b4;">nil?
</span><span> </span><span style="color:#b48ead;">return </span><span>'</span><span style="color:#a3be8c;">{}</span><span>'
</span><span> </span><span style="color:#b48ead;">end
</span><span> @</span><span style="color:#bf616a;">post </span><span>= </span><span style="color:#ebcb8b;">Post</span><span>.create parameters["</span><span style="color:#a3be8c;">post</span><span>"]
</span><span> render "</span><span style="color:#a3be8c;">posts/show</span><span>"
</span><span> </span><span style="color:#b48ead;">end
</span><span>
</span><span> get </span><span style="color:#a3be8c;">:show</span><span>, </span><span style="color:#a3be8c;">map: </span><span>"</span><span style="color:#a3be8c;">:id</span><span>" </span><span style="color:#b48ead;">do
</span><span> @</span><span style="color:#bf616a;">post </span><span>= </span><span style="color:#ebcb8b;">Post</span><span>[params[</span><span style="color:#a3be8c;">:id</span><span>]]
</span><span> render "</span><span style="color:#a3be8c;">posts/show</span><span>"
</span><span> </span><span style="color:#b48ead;">end
</span><span>
</span><span> put </span><span style="color:#a3be8c;">:update</span><span>, </span><span style="color:#a3be8c;">map: </span><span>"</span><span style="color:#a3be8c;">:id</span><span>", </span><span style="color:#a3be8c;">csrf_protection: </span><span style="color:#d08770;">false </span><span style="color:#b48ead;">do
</span><span> @</span><span style="color:#bf616a;">post</span><span>.update params
</span><span> render "</span><span style="color:#a3be8c;">posts/show</span><span>"
</span><span>
</span><span> </span><span style="color:#b48ead;">end
</span><span>
</span><span> delete </span><span style="color:#a3be8c;">:destroy</span><span>, </span><span style="color:#a3be8c;">map: </span><span>"</span><span style="color:#a3be8c;">:id</span><span>", </span><span style="color:#a3be8c;">csrf_protection: </span><span style="color:#d08770;">false </span><span style="color:#b48ead;">do
</span><span> @</span><span style="color:#bf616a;">post </span><span>= </span><span style="color:#ebcb8b;">Post</span><span>[params[</span><span style="color:#a3be8c;">:id</span><span>]]
</span><span> @</span><span style="color:#bf616a;">post</span><span>.delete </span><span style="color:#b48ead;">unless </span><span>@</span><span style="color:#bf616a;">post</span><span>.</span><span style="color:#96b5b4;">nil?
</span><span> </span><span style="color:#b48ead;">end
</span><span>
</span><span style="color:#b48ead;">end
</span></code></pre>
<h3 id="cors">CORS</h3>
<p>We will be using Ember-CLI’s proxy option so we don’t have to worry about CORS. However for production you may want to include it.</p>
<p>In <code>app/app.rb</code> Add the following lines of code to your <code>App</code> class</p>
<pre data-lang="ruby" style="background-color:#2b303b;color:#c0c5ce;" class="language-ruby "><code class="language-ruby" data-lang="ruby"><span>use </span><span style="color:#ebcb8b;">Rack</span><span>::Cors </span><span style="color:#b48ead;">do
</span><span> allow </span><span style="color:#b48ead;">do
</span><span> origins '</span><span style="color:#a3be8c;">*</span><span>'
</span><span> resource '</span><span style="color:#a3be8c;">*</span><span>', </span><span style="color:#a3be8c;">:headers </span><span>=> </span><span style="color:#a3be8c;">:any</span><span>, </span><span style="color:#a3be8c;">:methods </span><span>=> [</span><span style="color:#a3be8c;">:get</span><span>, </span><span style="color:#a3be8c;">:post</span><span>, </span><span style="color:#a3be8c;">:options</span><span>, </span><span style="color:#a3be8c;">:delete</span><span>, </span><span style="color:#a3be8c;">:patch</span><span>]
</span><span> </span><span style="color:#b48ead;">end
</span><span style="color:#b48ead;">end
</span></code></pre>
<p>If you look at the Rack/CORS documentation, you can specify URLs that are allowed to access your API by updating the origins method.</p>
<pre data-lang="ruby" style="background-color:#2b303b;color:#c0c5ce;" class="language-ruby "><code class="language-ruby" data-lang="ruby"><span>origins '</span><span style="color:#a3be8c;">localhost:4200</span><span>' </span><span style="color:#65737e;"># Ember CLI App
</span></code></pre>
<p>Before moving on to Part 2 make sure your Padrino app is up and running. Use <code>bundle exec padrino start</code>.</p>
<h3 id="summary">Summary</h3>
<p>That’s it for part 1. We’ve successfully made an API compatible with Ember’s ActiveModelAdapter. We’ve disabled CSRF where necessary and enabled CORS.</p>
<p><a href="/articles/using-padrino-with-ember-cli-part-2">Click here to view part 2 for implementing the Ember side of the project</a>.</p>