Using Angular with Rails 5, by Julio Sampaio Author, Apr 12, 2021
You’ve heard this story before. You’ve got an application running on your decentralized, fully working back-end API, and a front end built with any common toolset.
Now, you want to keep using Angular. Or maybe you’re just looking for a way to integrate Angular with your Rails project because you prefer it that way. We don’t blame you.
With this approach, you can take the best of both worlds and decide if you want to use Rails or Angular functionality to format things.
What will we build
There’s no need to worry. This tutorial was written for that purpose. We’ll delve into how to create a fully efficient CRUD application on a user domain.
At the end of this article, you’ll learn some basic concepts around Angular and how to build a Rails back-end project that integrates directly with the front-end Angular, as shown below.
User CRUD made with Rails and Angular
The application will handle all four CRUD operations involving user domains retrieved from the external bogus test network service. The application will be built on top of the MVC architecture, with detailed explanations for each Angular layer to help you better understand how things fit together. The style is determined by Bootstrap.
Set up the
As you may have guessed, we will need the following software.
- Ruby (I chose the 2.7.0 Preview1 version).
- Ruby and Rails (I use version 5.0.7.2).
- Node.js (I’m using V13.7.0).
- Yarn (at least version 1.22.4
Make sure everything is installed properly. Then, we can move on to the project. Select a folder you like and run the following command.
rails new crud-rails-angular
Copy the code
Wait for the setup to complete and open the project in your favorite IDE. In this article, we’ll use VS Code because it’s simple, powerful, and accepts Rails and Angular syntax smoothly.
If you’ve been using Rails 5 for a while, you may have noticed that its new command in _Gemfile_ produces an error for SQLite configuration. It doesn’t have a minimal version, which makes it run incorrectly. Let’s fix this and update it to
Gem 'sqlite3', '~ > 1.3.10'Copy the code
Perfect!
Webpacker set
The best way to manage javascripts like applications in Rails is through Webpacker. It leverages Webpack’s behind-the-scenes capabilities, such as preprocessing and bundling JavaScript applications, such as Angular, into an existing Rails application.
To install it, simply add a new line to your _Gemfile_.
Gem 'webpacker', '~ > 4.3 x'Copy the code
This will ensure that you install a very new version. Next, run the following command.
bundle install
bundle exec rake webpacker:install
bundle exec rake webpacker:install:angular
Copy the code
The first command downloads and updates the added Rails dependencies.
The second command is equivalent to NPM install because it creates the _node_modules_ folder and installs a bunch of necessary Angular dependencies such as Babel, Sass, Browserlist, and Webpack. Now we have a Node and a Rails application in the same project.
In the latest command, we have the equivalent of NPM install Angular, which downloads all angular dependencies and makes them work with our Rails project.
At the end of these commands, you can also see the _package.json_ file created. All the dependencies we need are there, and you can add anything you need in the future.
In addition, some folders and files are created under _/app_ folders, such as the new _/javascript_. In this folder, you have created an _/hello_angular_ folder to support the start of your development.
To buy some time, I ask you to mirror your folder and file structure with the following structure.
Some Angular tweaks
Webpacker recommends a number of tweaks in your generated Rails project. So, let’s take some time to sort this out.
First, open your _application.js_ file in the _/packs_ folder (as shown above) and add the following code.
import "core-js/stable";
import "regenerator-runtime/runtime";
Copy the code
These imports act as an auxiliary force to stabilize the JavaScript environment in Rails projects.
Now we need to tell Rails where it must pick the output to its page. Once Webpacker has finished packaging, it generates a bunch of distributable static files that Rails must know about.
Go to the _application.html.erb_ file in the _app/views/layout_ folder and add it. Change the content of the head> tag to the following.
<head> <title>CrudRailsAngular</title> <base href="/" /> <! -- 1 --> <%= csrf_meta_tags %> <%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' % > < link rel = "stylesheet" href = "https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css" integrity="sha384-JcKb8q3iqJ61gNV9KGb8thSsNjpSL0n8PARn9HuZOnIxN0hoP+VmmDGMN5t9UJ0Z" crossorigin="anonymous" /> <! -- 2 --> <%= javascript_include_tag 'application', 'data-turbolinks-track': 'reload' %> <%= javascript_pack_tag 'application' %> <! -- 3 --> </head>Copy the code
Let’s break it down.
- Here, we’re going to add
base
Tag, which tells Rails where to look when the application starts. - We’ll use Bootstrap to infer the style of the page, so we can just focus on the implementation.
- This is where you must place the Webpacker tag, which maps to the _/ Packs_ folder content (that is, the content Webpacker automatically generates after each compilation).
Models and databases
Next comes the database setup. To make things faster, we’ll create a new model called User. This is the command you must run to implement it.
rails g scaffold User name:string age:integer address:text && rake db:migrate
Copy the code
It will create all the folders and files for our model that we will need to enable Rails to manipulate the database information (from SQLite) and store it in our CRUD operations.
You will then see that a new file _xxx1_create_users.rb has been created under the _db/migrate/ folder. Open it and you will see the newly created CreateUsers record.
In the _app/models/_ folder, you will see the User model created at that time in the _user.rb_ file.
Now, open the _db/seeds.rb_ file and add the following code.
User.create(name: 'Luke Wan', age: 23, address: '123 Test St.')
User.create(name: 'Mary Poppins', age: 41, address: '123 ABC St.')
User.create(name: 'John Neilman', age: 76, address: '000 Test St.')
Copy the code
This code will initialize our Users table with some data at startup. Save it and run the command.
This will seed the table with the commands listed above. Next, you can go into the SQLite database and check for success by issuing commands.
sqlite3 db/development.sqlite3
Copy the code
Then, select the data for the table.
You can see the results.
User component
You’ll need to install a few more dependencies to help convert HTML and CSS into our Rails pages; Add the Angular router, the form library, and ngX-Bootstrap, which we’ll use to facilitate bootstrap component creation and manipulation. Therefore, issue the following command.
yarn add @angular/router @angular/forms html-loader css-loader ngx-bootstrap
Copy the code
Before we jump into component code, however, there are a few important concepts we need to point out, starting with the anatomy of Angular components.
What is a component?
In Angular, components exist to connect your view to the application logic in TypeScript.
In other words, a component is like a container that holds all the logic your view needs to function. It defines the values that the view will render and controls their flow. It is equivalent to a “controller” in a similar framework.
To create a Component, all you need to do is define a new class that implements the OnInit interface and annotates the class with the @Component decorator.
export class UserIndexComponent implements OnInit { constructor() { ... } ngOnInit() { ... }}Copy the code
@ Component and the OnInit
The @Component decorator is important because it marks the class as an Angular Component and provides metadata configuration that helps Angular handle their processing, instantiation, and use at runtime.
The following metadata configuration is used as an example.
@Component({
selector: "users",
template: templateString,
})
Copy the code
Here, the selector tells Angular that the value provided is the CSS selector it can use to identify the current directive to the template; Yes, it is the same template provided in the next metadata property.
However, the OnInit interface is optional and is a good way to initialize things before a component completes its life cycle. It acts like a post-build method.
Dependency injection
Angular is a Dependency Injection_ (DI_) framework, a feature that makes it more modular and productive.
Angular dependencies can range from your services and repositories to any common object that you think is appropriate to inject elsewhere in your code.
To make a class “Injectable”, annotate it with the @Injectable decorator.
@Injectable({
providedIn: "root",
})
export class UserService {
...
}
Copy the code
ProvidedIn indicates which injector will provide the injectable object you are creating. The root value tells Angular that the injector should be application-level. There’s more, you can check it out here.
For example, to inject a class into a component, you ask Angular to do so in the component’s constructor.
constructor(
private userService: UserService,
) {}
Copy the code
It’s that simple!
Completed components
Below, you can find the final code list for our user components. Put it in _index.component.ts_ under _javascript/hello / _angular/app/_.
import { Component, OnInit, TemplateRef } from "@angular/core"; import { FormGroup, FormBuilder } from "@angular/forms"; import { BsModalRef, BsModalService } from "ngx-bootstrap/modal"; import templateString from "./index.component.html"; import { UserService } from ".. /user.service"; import { User } from ".. /user.class"; @Component({ selector: "users", template: templateString, }) export class UserIndexComponent implements OnInit { users: User[]; modalRef: BsModalRef; userForm: FormGroup; isNew: Boolean; constructor(public fb: FormBuilder, private userService: UserService, private modalService: BsModalService) {} public newUser(template: TemplateRef<any>) { this.reset(); this.modalRef = this.modalService.show(template); } public createUser() { this.userService.create(this.userForm.value).subscribe(() => { console.log("User created!" ); this.reset(); this.modalRef.hide(); }); } public editUser(user, template: TemplateRef<any>) { this.isNew = false; this.userForm = this.fb.group({ id: [user.id], name: [user.name], age: [user.age], address: [user.address], }); this.modalRef = this.modalService.show(template); } public updateUser() { const { id } = this.userForm.value; this.userService.update(id, this.userForm.value).subscribe(() => { console.log("User updated!" ); this.reset(); this.modalRef.hide(); }); } public deleteUser(id) { if (confirm("Are you sure?" )) { this.userService.delete(id).subscribe(() => { console.log("User deleted!" ); this.reset(); }); } } ngOnInit() { this.reset(); } public reset() { this.isNew = true; this.userService.getUsers().subscribe((users) => { this.users = users; }); this.userForm = this.fb.group({ id: [""], name: [""], age: [""], address: [""], }); }}Copy the code
The Users array will hold the current table data listed on the screen and get it from the reset method, which in turn calls our Rails API through the UserService (which will be created).
The userForm is just a reference to help create and update our users, because both operations will use the same form. IsNew can also help us determine which process we are currently in.
Here, we have a CRUD equivalent method for each operation. Each of them invokes its own UserService method to submit the process in the Rails API.
We also need to set up the HTML module to convert our template to HTML (we’ll see more about modules soon). So, open the _html.d.ts_ file in the same folder and add.
declare module "*.html" {
const content: string;
export default content;
}
Copy the code
Angular services and models
Let’s continue our discussion of Angular UserService creation. Angular is a framework, just like Rails. So, that means it’s OK to follow their rules, even if it means having duplicate (or very similar) models, for example.
What is a model?
Angular models are simple objects that hold meaningful data attributes together (that is, they represent a concise part of your domain). They are just like any other model in most languages and frameworks.
Having your data in one place helps a lot, rather than reusing it throughout the code like we did with the user model.
export class User {
constructor(public id: number, public name: string, public age: number, public address: string) {}
}
Copy the code
Remember, this is TypeScript, so your model’s properties must always have a defined type.
Create a new file named _user.class.ts_ under the _javascript/hello_angular/app/user/_ folder and put the above code in it.
What is the service?
Services are a broad concept, but we can think of them as well-defined, purposeful objects. They help components accomplish more complex logic by providing them with processed and transformed data, often from external services or databases.
A service does not require any specific annotations or interfaces; You simply create a class and make it _ injectable _, as we saw earlier. You can then inject it into your component.
Observable services
Another interesting feature of Angular is that it allows you to use RxJS in your classes.
For example, Angular’s default HTTP client, which we use to get information from external services, returns RxJSObservables. That’s why, when you call any of our UserService methods in a user component, you might subscribe to an Observable result.
this.userService.getUsers().subscribe((users) => {
this.users = users;
});
Copy the code
Note that if you are not familiar with RxJS, I strongly recommend that you read it brieflyThe document; It’s not hard! 😉
Also, in the _javascript/hello_angular/app/user/_ folder, create another file named _user.service.ts_. This is what it says.
import { Injectable } from "@angular/core"; import { HttpClient, HttpHeaders } from "@angular/common/http"; import { map } from "rxjs/operators"; import { Observable } from "rxjs"; import { User } from "./user.class"; @Injectable({ providedIn: "root", }) export class UserService { constructor(private http: HttpClient) {} httpOptions = { headers: new HttpHeaders({ "Content-Type": "application/json", }), }; getUsers(): Observable<User[]> { return this.http.get("/users.json").pipe( map((users: User[]) => users.map((user) => { return new User(user.id, user.name, user.age, user.address); }))); } create(user): Observable<User> { return this.http.post<User>("/users.json", JSON.stringify(user), this.httpOptions); } update(id, user): Observable<User> { return this.http.put<User>("/users/" + id + ".json", JSON.stringify(user), this.httpOptions); } delete(id) { return this.http.delete<User>("/users/" + id + ".json", this.httpOptions); }}Copy the code
Can you see any similarities between this file and the component we just created? This is because we need corresponding operations to support operations in the component.
Note that HttpClient must also be injected into the constructor of the class, so we can use it with the class.
Each operation makes an HTTP call to our Rails API, which is automatically generated.
view
Angular views use templates. A template is a layered mixture of HTML and JavaScript that tells Angular how to render each component.
However, before we build our view further, let’s first understand how Angular separates its template system.
Presents the instructions
Because Angular templates are basically dynamic, some _ directive _ is needed to drive Angular to render things the right way.
Directives are simple classes with an @Directive decorator, just like components. Yes, @Component inherits @Directive, so it is also a formal Directive.
However, there are two other types: _ struct _ directives and _ attribute _ directives.
Structural instruction
These directives represent conditions and loop structures for translating from JavaScript to Angular templates. They help to make the template as dynamic as possible, just like when you program in your Vanilla JavaScript code. Take the following example.
<tr *ngFor="let user of users">
<td>{{ user.name }}</td>
</tr>
Copy the code
The _*ngFor_ directive tells Angular to traverse users and print each user’s name to the DOM.
Attribute instructions
These directly affect the appearance or behavior of elements. Take the following example.
<form [formGroup]="userForm" (ngSubmit)="isNew ? createUser() : updateUser()" novalidate></form>
Copy the code
Here, we modify the form’s behavior by conditionally setting the form’s Submit function, and use Angular’s FormGroup to data bind each form input.
Data binding
Creating forms with a Web framework can be a tricky, error-prone task if data binding is not provided.
Angular supports two-way data binding, which means you can connect your template fragments directly to components and vice versa.
The form above is a good example of the power of FormGroup data binding. It automatically binds each form field to the userForm object created within our component.
For example, in the editUser method, you can see the opposite version of the binding, where the value of userForm’ is set in the component and should be reflected in the form in the view.
Building an indexed view
We split the contents of _index.component.html_ into two parts. That’s the first part.
<div class="container pricing-header px-3 py-3 pt-md-5 pb-md-4 mx-auto text-center">
<h1 class="display-4">User's Listing</h1>
<p class="lead">A quick CRUD example of how to integrate Rails with Angular</p>
<table class="table">
<tr>
<th>Id</th>
<th>Name</th>
<th>Age</th>
<th>Address</th>
<th>Actions</th>
</tr>
<tbody>
<tr *ngFor="let user of users">
<td>{{ user.id }}</td>
<td>{{ user.name }}</td>
<td>{{ user.age }}</td>
<td>{{ user.address }}</td>
<td colspan="2">
<button class="btn btn-secondary" (click)="editUser(user, template)">Edit</button>
|
<button class="btn btn-danger" (click)="deleteUser(user.id)">Delete</button>
</td>
</tr>
</tbody>
</table>
<button class="btn btn-primary float-right mt-4" (click)="newUser(template)">Insert New</button>
</div>
Copy the code
Most of it is made up of plain HTML. We will not discuss the Bootstrap class in detail.
The important part here is the ngFor directive on the table row. It helps to iterate over the Users array (remember?). , via {{… The}} operator prints each of its attributes to HTML output.
Whenever you want to add a DOM event, such as _onClick_, simply enclose the event name in parentheses and add the component function that will be called when clicked.
Build a modal view
The second part has to do with the modal content, so add it under the previous part.
<ng-template #template> <div class="modal-header"> <h4 class="modal-title pull-left">{{ isNew ? "New User" : "Update User" }}</h4> <button type="button" class="close pull-right" aria-label="Close" (click)="modalRef.hide()"> <span aria-hidden="true">× </span> </button> </div> <div class="modal-body"> <form [formGroup]="userForm" (ngSubmit)="isNew ? createUser() : updateUser()" novalidate> <input type="hidden" formControlName="id" class="form-control" /> <div class="form-group"> <label>Name</label> <input type="text" formControlName="name" class="form-control" /> </div> <div class="form-group"> <label>Age</label> <input type="text" formControlName="age" class="form-control" /> </div> <div class="form-group"> <label>Address</label> <textarea class="form-control" formControlName="address" rows="3"></textarea> </div> <button type="submit" class="btn btn-primary">Submit</button> </form> </div> </ng-template>Copy the code
Note that we are using the
tag, which allows you to anchor elements between HTML and Angular. The template ID comes after the # symbol.
In the form, also notice that we are using the isNew component variable to verify that the current use of the form is related to the creation or updating of the user.
Finally, we need to inject the entire _hello\_angular_ application into the Rails _index.html.erb_ page. Therefore, open the file in the _views/users/_ folder and change its contents to the following.
<hello-angular>We're almost done... </hello-angular> <%= javascript_pack_tag 'hello_angular' %>Copy the code
Presents the module
Now we need to tell Angular where to find things. This happens in the configuration of its modules.
Let’s start by adding content to _app-bootstrap.module.ts_.
import { NgModule } from "@angular/core";
import { CommonModule } from "@angular/common";
import { ModalModule } from "ngx-bootstrap/modal";
@NgModule({
imports: [CommonModule, ModalModule.forRoot()],
exports: [ModalModule],
})
export class AppBootstrapModule {}
Copy the code
This is limited to the bootstrap component that we inherit from NGX-Bootstrap. The only component we’re going to use now is Bootstrap Modal.
Then, open the _app-routing.module.ts_ file and change its contents to the following.
import { RouterModule, Routes } from "@angular/router";
import { NgModule } from "@angular/core";
import { UserIndexComponent } from "./user/index/index.component";
const appRoutes: Routes = [
{ path: "users", component: UserIndexComponent },
{ path: "", redirectTo: "/users", pathMatch: "full" },
];
@NgModule({
imports: [RouterModule.forRoot(appRoutes, { scrollPositionRestoration: "enabled" })],
exports: [RouterModule],
})
export class AppRoutingModule {}
Copy the code
This ensures that Angular matches the correct User component when calling the _/users_ path.
Finally, register all of these components in the main AppModule class. Open the _app.module.ts_ file and make sure it looks like this.
import { BrowserModule } from "@angular/platform-browser";
import { NgModule } from "@angular/core";
import { HttpClientModule } from "@angular/common/http";
import { FormsModule, ReactiveFormsModule } from "@angular/forms";
import { AppComponent } from "./app.component";
import { AppRoutingModule } from "./app-routing.module";
import { AppBootstrapModule } from "./app-boostrap.module";
import { UserIndexComponent } from "./user/index/index.component";
@NgModule({
declarations: [AppComponent, UserIndexComponent],
imports: [HttpClientModule, AppRoutingModule, BrowserModule, FormsModule, ReactiveFormsModule, AppBootstrapModule],
providers: [],
bootstrap: [AppComponent],
})
export class AppModule {}
Copy the code
Here, everything is mapped. Configuration from our form, HTTP client, and user components to the Bootstrap module, and routing.
Complete the configuration
Before we get into the test, we need to complete a few things, starting with the _app.component.ts_ file.
import { Component } from "@angular/core"; @Component({ selector: "hello-angular", template: "<router-outlet></router-outlet>", }) export class AppComponent { name = "Angular!" ; }Copy the code
The main application component needs to know how to route the path, so RouterOutlet will do the job.
Then, we need to make sure that Webpacker understands the HTML extensions we’ve been using so far. To do this, open the _webPacker.yml_ file, under the _/config_ folder, search the _extensions_ section and add the following projects.
Webpacker recognizes only the built-in TypeScript loader that comes with Angular by default. We need to work with HTML, which is why we installed the _HTml-loader_ dependency earlier. To set it up, open the _environment.js_ file in the _config/webpack_ folder and add the following loader configuration.
environment.loaders.append("html", {
test: /\.html$/,
use: [
{
loader: "html-loader",
options: {
minimize: true,
},
},
],
});
Copy the code
Finally, to prevent our Angular service from receiving errors in its HTTP calls, we need to disable the CSRF token checking that Rails does. To do this, open the _application_Controller. rb_ file in the _app/controllers_ folder and change its contents to the following.
class ApplicationController < ActionController::Base
protect_from_forgery with: :null_session
end
Copy the code
test
Here it is! This may seem a bit tricky because the setup requires a lot of customization, but the results are worth it.
To test, save everything and start the server by issuing the Rails S command.
And then, into your web browser, enter the address http://localhost:3000/users. Go ahead and play with the CRUD Web application.
conclusion
It’s a long road to get this CRUD up and running. After your first experiment, you’ll find that things get easier for your future projects. I hope this project helps establish a starting point for those who want to jumpstart projects by joining these two technologies.
While we don’t have an open source scaffolding project to help it, we rely on each other’s efforts to have such materials. Now, it’s your turn; Fork the project (or create it from scratch) and start your customization.
The GitHub repository for this example can be found here. Have fun!