ruby on rails
There are plenty tutorials online which show how to create your first app. This tutorial will go a step further and explain line-by-line how to create a more complex Ruby On Rails application.
在线上有很多教程,展示了如何创建您的第一个应用程序。 本教程将更进一步,逐行说明如何创建更复杂的Ruby On Rails应用程序。
Throughout the whole tutorial, I will gradually introduce new techniques and concepts. The idea is that with every new section you should learn something new.
在整个教程中,我将逐步介绍新技术和新概念。 我们的想法是,在每个新部分中,您都应该学习一些新知识。
The following topics will be discussed throughout this guide:
本指南将讨论以下主题:
It’s going to be a platform where you could search and meet like-minded people.
这将是一个平台,您可以在这里搜索并结识志趣相投的人。
Main functionalities which the app will have:
该应用程序将具有的主要功能:
You can see how the complete application is going to look.
您可以看到 完整的应用程序 外观。
And you can find the complete project’s source code on GitHub.
您可以在 GitHub上 找到完整项目的源代码 。
Table of Contents
目录
Introduction and Setup Prequisites Setup Create a new app
简介和设置 先决条件 设置 创建新应用
Layout Home page Bootstrap Navigation bar Style sheets
布局 主页 Bootstrap 导航栏 样式表
Posts Authentication Helpers Testing Main feed Single post Specific branches Service objects Create a new post
帖子 身份验证 帮助程序 测试 主提要 单个帖子 特定分支 服务对象 创建新帖子
Instant messaging Private conversation Contacts Group conversation Messenger
即时消息 私人对话 联系人 群组对话 Messenger
Notifications Contact requests Conversations
通知 联系人请求 对话
I will try to explain every line of code and how I came up with the solutions. I think it is entirely possible for a total beginner to complete this guide. But keep in mind that this tutorial covers some topics which are beyond the basics.
我将尝试解释每一行代码以及我如何提出解决方案。 我认为一个完整的初学者完全有可能完成本指南。 但是请记住,本教程涵盖了一些基础知识以外的主题。
So if you are a total beginner, it’s going to be harder, because your learning curve is going to be pretty steep. I will provide links to resources where you could get some extra information about every new concept we touch.
因此,如果您是一个初学者,那么它将变得更加困难,因为您的学习曲线将非常陡峭。 我将提供指向资源的链接,您可以在其中获得有关我们接触的每个新概念的更多信息。
Ideally, it’s best if you are aware of the fundamentals of:
理想情况下,最好是了解以下基本知识:
HTML, CSS, Bootstrap, JavaScript, jQuery
HTML , CSS , 引导程序 , JavaScript , jQuery
Ruby, Ruby On Rails
Ruby , Ruby On Rails
Git
吉特
I assume that you have already set up your basic Ruby On Rails development environment. If not, check RailsInstaller.
我假设您已经设置了基本的Ruby On Rails开发环境。 如果不是,请检查RailsInstaller 。
I had been developing on Windows 10 for a while. At first it was okay, but after some time I got tired of overcoming mystical obstacles which were caused by Windows. I had to keep figuring out hack ways to make my applications work. I’ve realized that it isn’t worth my time. Overcoming those obstacles didn’t give me any valuable skills or knowledge. I was just spending my time by duct taping Windows 10 setup.
我已经在Windows 10上开发了一段时间。 一开始还可以,但是一段时间后,我厌倦了克服Windows造成的神秘障碍。 我必须不断找出使我的应用程序正常工作的破解方法。 我意识到这不值得我花时间。 克服这些障碍并没有给我任何有价值的技能或知识。 我只是在花时间点Windows 10安装程序。
So I switched to a virtual machine instead. I chose to use Vagrant to create a development environment and PuTTY to connect to a virtual machine. If you want to use Vagrant too, this is the tutorial which I found useful.
所以我改用虚拟机 。 我选择使用Vagrant创建开发环境,并使用PuTTY连接到虚拟机。 如果您也想使用Vagrant,这是我发现有用的教程 。
We are going to use PostgreSQL as our database. It is a popular choice among Ruby On Rails community. If you haven’t created any Rails apps with PostgreSQL yet, you may want to check this tutorial.
我们将使用PostgreSQL作为数据库。 在Ruby On Rails社区中,它是一个受欢迎的选择。 如果尚未使用PostgreSQL创建任何Rails应用,则可能需要查看本教程 。
Once you are familiar with PostgreSQL, navigate to a directory where you keep your projects and open a command line prompt.
熟悉PostgreSQL后,导航到保存项目的目录并打开命令行提示符。
To generate a new app run this line:
要生成新应用,请运行以下行:
rails new collabfield --database=postgresql
Collabfield
, that’s how our applications is going to be called. By default Rails uses SQlite3, but since we want to use PostgreSQL as our database, we need to specify it by adding:
Collabfield
,这就是我们的应用程序将被调用的方式。 默认情况下,Rails使用SQlite3,但是由于我们要使用PostgreSQL作为数据库,因此需要通过添加以下内容来指定它:
--database=postgresql
Now we should’ve successfully generated a new application.
现在,我们应该已经成功生成了一个新应用程序。
Navigate to a newly created directory by running the command:
通过运行以下命令导航到新创建的目录:
cd collabfield
And now we can run our app by entering:
现在,我们可以通过输入以下内容来运行我们的应用程序:
rails s
We just started our app. Now we should be able to see what we got so far. Open a browser and go to http://localhost:3000. If everything went well, you should see the Rails signature welcome page.
我们刚刚启动了我们的应用程序。 现在我们应该能够看到到目前为止所取得的成就。 打开浏览器,然后转到http:// localhost:3000 。 如果一切顺利,您应该看到Rails签名的欢迎页面。
Time to code. Where should we start? Well, we can start wherever we want to. When I build a new website, I like to start by creating some kind of basic visual structure and then build everything else around that. Let’s do just that.
该写代码了。 我们应该从哪里开始? 好吧,我们可以从任何地方开始。 当我建立一个新网站时,我喜欢从创建某种基本的视觉结构开始,然后围绕它构建其他所有内容。 让我们开始吧。
When we go to http://localhost:3000, we see the Rails welcome page. We’re going to switch this default page with our own home page. In order to do that, generate a new controller called Pages
. If you are not familiar with Rails controllers, you should skim through the Action Controller to get an idea what the Rails controller is. Run this line in your command prompt to generate a new controller.
当我们转到http:// localhost:3000时 ,我们会看到Rails的欢迎页面。 我们将用我们自己的主页切换此默认页面。 为此,请生成一个名为Pages
的新控制器。 如果您不熟悉Rails控制器,则应该浏览一下Action Controller ,以了解什么是Rails控制器。 在命令提示符下运行此行以生成新的控制器。
rails g controller pages
This rails generator should have created some files for us. The output in the command prompt should look something like this:
这个rails生成器应该已经为我们创建了一些文件。 命令提示符中的输出应如下所示:
We are going to use this PagesController
to manage our special and static pages. Now open the Collabfield project in a text editor. I use Sublime Text, but you can use whatever you want to.
我们将使用此PagesController
来管理特殊页面和静态页面。 现在,在文本编辑器中打开Collabfield项目。 我使用Sublime Text ,但是您可以使用任何您想要的东西。
Open a file pages_controller.rb
打开文件pages_controller.rb
app/controllers/pages_controller.rb
We’ll define our home page here. Of course we could define home page in a different controller and in different ways. But usually I like to define the home page inside the PagesController
.
我们将在此处定义我们的主页。 当然,我们可以用不同的控制器和不同的方式定义主页。 但是通常我喜欢在PagesController
内定义主页。
When we open pages_controller.rb
, we see this:
当我们打开pages_controller.rb
,我们看到:
It’s an empty class, named PagesController
, which inherits from the ApplicationController
class. You can find this class’s source code in app/controllers/application_controller.rb
.
它是一个空类,名为PagesController
,它是从ApplicationController
类继承的。 您可以在app/controllers/application_controller.rb
找到此类的源代码。
All our controllers, which we will create, are going to inherit from ApplicationController
class. Which means that all methods defined inside this class are going to be available across all our controllers.
我们将创建的所有控制器都将从ApplicationController
类继承。 这意味着在该类中定义的所有方法将在所有控制器中都可用。
We’ll define a public method named index
, so it can be callable as an action:
我们将定义一个名为index
的公共方法,以便可以将其作为操作调用:
As you may have read in the Action Controller, routing determines which controller and its public method (action) to call. Let’s define a route, so when we open our root page of the website, Rails knows which controller and its action to call. Open a routes.rb
file in app/config/routes.rb
.
您可能已经在Action Controller中读过了,路由确定了要调用的控制器及其公共方法(action)。 让我们定义一条路线,以便当我们打开网站的根页面时,Rails知道要调用哪个控制器及其动作。 在app/config/routes.rb
打开一个routes.rb
文件。
If you don’t know what Rails routes is, it is a perfect time to get familiar by reading the Rails Routing.
如果您不知道Rails的路线是什么,那么这是阅读Rails Routing的最佳时机。
Insert this line:
插入此行:
root to: 'pages#index'
Your routes.rb
file should look like this:
您的routes.rb
文件应如下所示:
Hash symbol #
in Ruby represents a method. As you remember an action is just a public method, so pages#index
says “call the PagesController
and its public method (action) index
.”
Ruby中的哈希符号#
表示方法。 您还记得操作只是一个公共方法,所以pages#index
表示“调用PagesController
及其公共方法(操作) index
。
If we went to our root path http://localhost:3000, the index action would be called. But we don’t have any templates to render yet. So let’s create a new template for our index
action. Go to app/views/pages
and create an index.html.erb
file inside this directory. Inside this file we can write our regular HTML+ Embedded Ruby code. Just write something inside the file, so we could see the rendered template in the browser.
如果我们转到根路径http:// localhost:3000 ,则将调用index操作。 但是我们还没有要渲染的模板。 因此,让我们为index
操作创建一个新模板。 转到app/views/pages
并在此目录中创建index.html.erb
文件。 在此文件中,我们可以编写常规HTML + Embedded Ruby代码。 只需在文件中写入内容,就可以在浏览器中看到渲染的模板。
Home page
Now when we go to http://localhost:3000, we should see something like this instead of the default Rails information page.
现在,当我们转到http:// localhost:3000时 ,我们应该看到类似这样的内容,而不是默认的Rails信息页面。
Now we have a very basic starting point. We can start introducing new things to our website. I think it’s time to create our first commit.
现在我们有了一个非常基本的起点。 我们可以开始向我们的网站介绍新事物。 我认为是时候创建我们的第一次提交了。
In your command prompt run:
在命令提示符下运行:
git status
And you should see something like this:
而且您应该看到类似以下内容的内容:
If you don’t already know, when we generate a new application, a new local git repository is initialized.
如果您还不知道,那么当我们生成新应用程序时,将初始化一个新的本地git存储库。
Add all current changes by running:
通过运行以下命令添加所有当前更改:
git add -A
Then commit all changes by running:
然后通过运行以下命令来提交所有更改:
git commit -m "Generate PagesController. Initialize Home page"
If we ran this:
如果我们运行此命令:
git status
We would see that there’s nothing to commit, because we just successfully committed all changes.
我们会看到没有什么要提交的,因为我们只是成功地提交了所有更改。
For the navigation bar and the responsive grid system we’re going to use the Bootstrap library. In order to use this library we have to install the
对于导航栏和自适应网格系统,我们将使用Bootstrap库。 为了使用此库,我们必须安装
bootstrap-sass gem. Open the Gemfile
in your editor.
自举宝石。 在编辑器中打开Gemfile
。
collabfield/Gemfile
Add a bootstrap-sass
gem to the Gemfile. As the documentation says, you have to ensure that sass-rails
gem is present too.
向Gemfile中添加一个bootstrap-sass
无效的gem。 如文档所述,您必须确保也存在sass-rails
gem。
...
gem 'bootstrap-sass', '~> 3.3.6'
gem 'sass-rails', '>= 3.2'
...
Save the file and run this to install newly added gems:
保存文件并运行此命令以安装新添加的gem:
bundle install
If you are still running the application, restart the Rails server to make sure that new gems are available. To restart the server simply shutdown it by pressing Ctrl + C
and run rails s
command again to boot the server.
如果您仍在运行应用程序,请重新启动Rails服务器以确保新的gems可用。 要重新启动服务器,只需按Ctrl + C
其关闭,然后再次运行rails s
命令来启动服务器。
Go to assets
to open the application.css
file:
转到assets
以打开application.css
文件:
app/assets/stylesheets/application.css
app/assets/stylesheets/application.css
Below all the commented text add this:
在所有注释文本下面添加以下内容:
...
@import "bootstrap-sprockets";
@import "bootstrap";
Now change the application.css
name to application.scss
. This is necessary in order to use Bootstrap library in Rails, also it allows us to use Sass features.
现在,将application.css
名称更改为application.scss
。 为了在Rails中使用Bootstrap库,这是必要的,它还允许我们使用Sass功能。
We want to control the order in which all .scss
files are rendered, because in the future we might want to create some Sass variables. We want to make sure that our variables are going to be defined before we use them.
我们想控制所有.scss
文件的呈现顺序,因为将来我们可能要创建一些Sass变量。 我们要确保在使用变量之前先定义它们。
To accomplish it, remove those two lines from the application.scss
file:
为此,请从application.scss
文件中删除这两行:
*= require_self
*= require_tree .
We’re almost able to use Bootstrap library. There’s a one more thing which we have to do. As the bootstrap-sass docs says, Bootstrap JavaScript is dependent on jQuery library. To use jQuery with Rails, you have to add jquery-rails gem.
我们几乎可以使用Bootstrap库。 我们还有另一件事要做。 正如bootstrap-sass的文档所述,Bootstrap JavaScript依赖于jQuery库。 要将jQuery与Rails一起使用,您必须添加jquery-rails gem。
gem 'jquery-rails'
Run…
跑…
bundle install
…again, and restart the server.
…再次,然后重新启动服务器。
Last step is to require Bootstrap and jQuery in the application’s JavaScript file. Go to application.js
最后一步是在应用程序JavaScript文件中需要Bootstrap和jQuery。 转到application.js
app/assets/javascripts/application.js
Then add the following lines in the file:
然后在文件中添加以下行:
//= require jquery
//= require bootstrap-sprockets
Commit the changes:
提交更改:
git add -A
git commit -m "Add and configure bootstrap gem"
For the navigation bar we’ll use Bootstrap’s navbar component as the starting point and then quite modify it. We will store our navigation bar inside a partial template.
对于导航栏,我们将使用Bootstrap的navbar组件作为起点,然后对其进行相当的修改。 我们将把导航栏存储在部分模板中 。
We’re doing this because it’s better to keep every component of the app in separate files. It allows to test and manage app’s code much easier. Also we can reuse those components in other parts of the app, without duplicating the code.
我们这样做是因为最好将应用程序的每个组件都放在单独的文件中。 它可以更轻松地测试和管理应用程序的代码。 同样,我们可以在应用程序的其他部分重用这些组件,而无需复制代码。
Navigate to:
导航:
views/layouts
Create a new file:
创建一个新文件:
_navigation.html.erb
For partials we use underscore prefix, so the Rails framework can distinguish it as a partial. Now copy and paste navbar component from Bootstrap docs and save the file. To see the partial on the website, we have to render it somewhere. Navigate to views/layouts/application.html.erb
. This is the default file where everything gets rendered.
对于局部,我们使用下划线前缀,因此Rails框架可以将其区分为局部。 现在,从Bootstrap文档复制并粘贴navbar组件并保存文件。 要在网站上看到部分内容,我们必须将其渲染到某个地方。 导航到views/layouts/application.html.erb
。 这是呈现所有内容的默认文件。
Inside the file we see the following method:
在文件内部,我们看到以下方法:
<%= yield %>
It renders the requested template. To use ruby syntax inside the HTML file, we have to wrap it around with <% %>
(embedded ruby allows us to do that). To quickly learn the differences between ERB syntax, checkout this StackOverflow answer.
呈现请求的模板。 要在HTML文件中使用ruby语法,我们必须用<% %>
将其包装起来(嵌入的ruby可以做到这一点)。 要快速了解ERB语法之间的区别,请查看此StackOverflow答案 。
In Home page section we set the route to recognize the root URL. So whenever we send a GET request to go to a root page, PagesController‘sindex
action gets called. And that respective action (in this case the index
action) responds with a template which gets rendered with the yield
method. As you remember, our template for a home page is located at app/views/pages/index.html.erb
.
在首页部分中,我们设置了识别根URL的路由 。 因此,每当我们发送GET请求转到根页面时,就会调用PagesController'sindex
操作。 相应的动作(在本例中为index
动作)以模板响应,该模板通过yield
方法呈现。 您还记得,我们的主页模板位于app/views/pages/index.html.erb
。
Since we want to have a navigation bar across all pages, we’ll render our navigation bar inside the default application.html.erb
file. To render a partial file , simply use the render
method and pass partial’s path as an argument. Do it just above the yield
method like this:
由于我们希望所有页面都具有导航栏,因此我们将在默认的application.html.erb
文件中呈现导航栏。 要渲染部分文件,只需使用render
方法并将部分的路径作为参数传递。 像这样在yield
方法上方执行此操作:
...
<%= render 'layouts/navigation' %>
<%= yield %>
...
Now go to http://localhost:3000 and you should be able to see the navigation bar.
现在转到http:// localhost:3000 ,您应该可以看到导航栏。
As mentioned above, we’re going to modify this navigation bar. First let’s remove all and
elements. In the future we’ll create our own elements here. The
_navigation.html.erb
file should look like this now.
如上所述,我们将修改此导航栏。 首先,让我们删除所有和
元素。 将来,我们将在这里创建我们自己的元素。
_navigation.html.erb
文件现在应该看起来像这样。
We have a basic responsive navigation bar now. It’s a good time to create a new commit. In command prompt run the following commands:
现在,我们有一个基本的响应式导航栏。 现在是创建新提交的好时机。 在命令提示符下,运行以下命令:
git add -A
git commit -m "Add a basic navigation bar
We should change the navigation bar’s name from Brand
to collabfield
. Since Brand
is a link element, we should use a link_to
method to generate links. Why? Because this method allows us to easily generate URI paths. Open a command prompt and navigate to the project’s directory. Run the the following command:
我们应该将导航栏的名称从Brand
更改为collabfield
。 由于Brand
是一个链接元素,因此我们应该使用link_to
方法来生成链接。 为什么? 因为此方法使我们能够轻松生成URI路径。 打开命令提示符,然后导航到项目目录。 运行以下命令:
rails routes
This command outputs our available routes, which are generated by the routes.rb
file. As we see:
此命令输出我们的可用路由,这些路由是由routes.rb
文件生成的。 如我们所见:
Currently, we have only one route, the one which we’ve defined before. If you look at the given routes, you can see a Prefix
column. We can use those prefixes to generate a path to a wanted page. All we have to do is use a prefix name and add _path
to it. If we wrote the root_path
, that would generate a path to the root page. So let’s use the power of link_to
method and routes.
目前,我们只有一条路线,这是我们之前定义的路线。 如果查看给定的路线,则可以看到“ Prefix
列。 我们可以使用这些前缀来生成通向所需页面的路径。 我们要做的就是使用前缀名称,并在其中添加_path
。 如果我们写了root_path
,那将生成一个到根页面的路径。 因此,让我们使用link_to
方法和路由的功能。
Replace this line:
替换此行:
Brand
With this line:
用这行:
<%= link_to 'collabfield', root_path, class: 'navbar-brand' %>
Remember that whenever you don’t quite understand how a particular method works, just Google it and you will probably find its documentation with an explanation. Sometimes documentations are poorly written, so you might want to Google a little bit more and you might find a blog or a StackOverflow answer, which would help.
请记住,每当您不太了解特定方法的工作原理时,只需使用Google即可,并且可能会找到其说明文件。 有时文档写得不好,所以您可能想对Google多一点,可能会找到博客或StackOverflow答案,这将有所帮助。
In this case we pass a string as our first argument to add the element’s value, the second argument is needed for a path, this is where routes helps us to generate it. The third argument is optional, which is accumulated inside the options hash. In this case we needed to add
navbar-brand
class to keep our Bootstrap powered navigation bar to function.
在这种情况下,我们将字符串作为第一个参数传递来添加元素的值,路径需要第二个参数,这是路由帮助我们生成它的地方。 第三个参数是可选的,它累积在options哈希中。 在这种情况下,我们需要添加
navbar-brand
类,以保持由Bootstrap支持的导航栏正常运行。
Let’s do another commit for this small change. In the upcoming section we’ll start changing our app’s design, starting from the navigation bar.
让我们为这个小的更改做另一个提交。 在接下来的部分中,我们将从导航栏开始更改应用程序的设计。
git add -A
git commit -m "Change navigation bar's brand name from Brand to collabfield"
Let me introduce you how I structure my style sheet files. From what I know there aren’t any strong conventions on how to structure your style sheets in Rails. Everyone is doing it slightly differently.
让我向您介绍如何构造样式表文件。 据我所知,关于如何在Rails中构建样式表并没有任何强硬的约定。 每个人的操作都略有不同。
This is how I usually structure my files.
这就是我通常构造文件的方式。
Base directory — This is where I keep Sass variables and styles which are used throughout the whole app. For instance default font sizes and default elements’ styles.
基本目录 -这是我保存整个应用程序中使用的Sass变量和样式的位置。 例如默认字体大小和默认元素的样式。
Partials — Most of my styles go there. I keep all styles for separate components and pages in this directory.
Partials-我的大多数风格都去了那里。 我将所有样式保留在该目录中,用于单独的组件和页面。
Responsive — Here I define different style rules for different screen sizes. For example, styles for a desktop screen, tablet screen, phone screen, etc.
响应式 —在这里,我为不同的屏幕尺寸定义了不同的样式规则。 例如,桌面屏幕,平板电脑屏幕,电话屏幕等的样式。
First, let’s create a new repository branch by running this:
首先,通过运行以下命令创建一个新的存储库分支:
git checkout -b "styles"
We’ve just created a new git branch and automatically switched to it. From now on this is how we’re going to implement new changes to the code.
我们刚刚创建了一个新的git分支,并自动切换到了它。 从现在开始,这就是我们要对代码进行新更改的方式。
The reason for doing this is that we can isolate our currently functional version (master branch) and write a new code inside a project’s copy, without being afraid to damage anything.
这样做的原因是,我们可以隔离当前可以使用的版本(master分支),并在项目副本中编写新代码,而不必担心损坏任何东西。
Once we are complete with the implementation, we can just merge changes to the master
branch.
一旦完成了实现,就可以将更改合并到master
分支中。
Start by creating few directories:
首先创建几个目录:
app/assets/stylesheets/partials/layout
Inside the layout directory create a file navigation.scss
and inside the file add:
在布局目录中,创建一个文件navigation.scss
并在文件中添加:
With these lines of code we change navbar’s background and links color. As you may have noticed, a
selector is nested inside another declaration block. Sass allows us to use this functionality. !important
is used to strictly override default Bootstraps styles. The last thing which you may have noticed is that instead of a color name, we use a Sass variable. The reason for this is that we are going to use this color multiple times across the app. Let’s define this variable.
通过这些代码行,我们可以更改导航栏的背景并链接颜色。 正如你可能已经注意到, a
选择是嵌套在另一个声明块。 Sass允许我们使用此功能。 !important
用于严格覆盖默认的Bootstraps样式。 您可能注意到的最后一件事是,我们使用Sass变量而不是颜色名称。 原因是我们将在整个应用程序中多次使用这种颜色。 让我们定义这个变量。
First create a new folder:
首先创建一个新文件夹:
app/assets/stylesheets/base
Inside the base directory create a new file variables.scss
. Inside the file define a variable:
在基本目录中,创建一个新文件variables.scss
。 在文件内定义一个变量:
$navbarColor: #323738;
If you tried to go to http://localhost:3000, you wouldn’t notice any style changes. The reason for that is that in the Bootstrap section we removed these lines:
如果您尝试转到http:// localhost:3000 ,则不会注意到任何样式更改。 原因是在Bootstrap部分中,我们删除了以下几行:
*= require_self
*= require_tree .
from application.scss
, to not automatically import all style files.
从application.scss
,不自动导入所有样式文件。
This means that now we have to import our newly created files to the main application.scss
file. The file should look like this now:
这意味着现在我们必须将新创建的文件导入到主application.scss
文件。 该文件现在应如下所示:
The reason for importing variables.scss
file at the top is to make sure that the variables are defined before we use them.
在顶部导入variables.scss
文件的原因是要确保在使用变量之前已定义它们。
Add some more CSS at the top of the navigation.scss
file:
在navigation.scss
文件顶部添加更多CSS:
Of course you can put this code at the bottom of the file if you want to. Personally, I order and group CSS code based on CSS selectors’ specificity. Again, everyone is doing it slightly differently. I put less specific selectors above and more specific selectors below. So for instance Type selectors go above Class selectors and Class selectors go above ID selectors.
当然,您可以根据需要将此代码放在文件的底部。 我个人根据CSS选择器的特殊性对CSS代码进行排序和分组。 同样,每个人的操作都略有不同。 我在上面放一些不太具体的选择器,在下面放一些更具体的选择器。 因此,例如,类型选择器位于类选择器之上,而类选择器则位于ID选择器之上。
Let’s commit changes:
让我们提交更改:
git add -A
git commit -m "Add CSS to the navigation bar"
We want to make sure that the navigation bar is always visible, even when we scroll down. Right now we don’t have enough content to scroll down, but we will in the future. Why don’t we give this feature to the navigation bar right now?
我们希望确保即使向下滚动导航栏也始终可见。 目前,我们没有足够的内容向下滚动,但将来会。 为什么我们现在不将此功能提供给导航栏?
To do that use Bootstrap class navbar-fixed-top
. Add this class to the nav
element, so it looks like this:
为此,请使用Bootstrap类navbar-fixed-top
。 将此类添加到nav
元素,因此如下所示:
Also we want to have collabfield
to be to the Bootstrap Grid System’sleft side boundaries. Right now it is to the viewport’s left side boundaries, because our class is currently container-fluid
. To change that, change the class to container
.
另外,我们希望使collabfield
成为Bootstrap Grid System的左侧边界。 现在是视口的左侧边界,因为我们的班级目前是container-fluid
。 要更改它,请将类更改为container
。
It should look like this:
它看起来应该像这样:
Commit the changes:
提交更改:
git add -A
git commit -m "
- in _navigation.html.erb add navbar-fixed-top class to nav.
- Replace container-fluid class with container"
If you go to http://localhost:3000, you see that the Home page
text is hidden under the navigation bar. That’s because of the navbar-fixed-top
class. To solve this issue, push the body down by adding the following to navigation.scss
:
如果转到http:// localhost:3000 ,则会看到Home page
文本隐藏在导航栏下方。 那是因为navbar-fixed-top
类。 要解决此问题,请通过将以下内容添加到navigation.scss
来向下推主体:
body {
margin-top: 50px;
}
At this stage the app should look like this:
在此阶段,应用程序应如下所示:
Commit the change:
提交更改:
git add -A
git commit -m "Add margin-top 50px to the body"
As you remember, we’ve created a new branch before and switched to it. It’s time to go back to the master
branch.
您还记得,我们之前已经创建了一个新分支并切换到该分支。 现在该回到master
分支了。
Run the command:
运行命令:
git branch
You can see the list of our branches. Currently we’re in the styles
branch.
您可以看到我们的分支机构列表。 目前,我们位于styles
分支。
To switch back to the master
branch, run:
要切换回master
分支,请运行:
git checkout master
To merge our all changes, which we did in the styles
branch, simply run:
要合并我们在styles
分支中所做的所有更改,只需运行:
git merge styles
The command merged those two branches and now we can see the summary of changes we made.
该命令合并了这两个分支,现在我们可以看到所做更改的摘要。
We don’t need styles
branch anymore, so we can delete it:
我们不再需要styles
分支,因此我们可以将其删除:
git branch -D styles
帖子 (Posts)
It’s almost a right time to start implementing the posts functionality. Since our app goal is to let users meet like-minded people, we have to make sure that posts’ authors can be identified. To achieve that, authentication system is required.
现在几乎是开始实施发布功能的正确时机。 由于我们的应用程序目标是让用户结识志趣相投的人,因此我们必须确保可以确定帖子的作者。 为此,需要身份验证系统。
认证方式 (Authentication)
For an authentication system we are going to use the devise gem. We could create our own authentication system, but that would require a lot of effort. We’ll choose an easier route. Also it’s a popular choice among Rails community.
对于身份验证系统,我们将使用devise gem 。 我们可以创建自己的身份验证系统,但这将需要大量的精力。 我们将选择一条更简单的路线。 这也是Rails社区中的流行选择。
Start by creating a new branch:
首先创建一个新分支:
git checkout -b authentication
Just like with any other gem, to set it up we’ll follow its documentation. Fortunately, it’s very easy to set up.
就像任何其他gem一样,我们将按照其文档进行设置。 幸运的是,它很容易设置。
Add to your Gemfile
添加到您的Gemfile
gem 'devise'
Then run commands:
然后运行命令:
bundle install
rails generate devise:install
You probably see some instructions in the command prompt. We won’t use mailers in this tutorial, so no further configuration is needed.
您可能会在命令提示符下看到一些说明。 在本教程中,我们将不使用邮件程序,因此不需要进一步的配置。
At this point, if you don’t know anything about Rails models, you should get familiar with them by skimming through Active Record and Active Modeldocumentations.
此时,如果您对Rails模型一无所知,则应通过浏览Active Record和Active Model文档来熟悉它们。
Now let’s use a devise generator to create a User
model.
现在,让我们使用一个devise生成器来创建一个User
模型。
rails generate devise User
Initialize a database for the app by running:
通过运行以下命令为应用程序初始化数据库:
rails db:create
Then run this command to create new tables in your database:
然后运行以下命令在数据库中创建新表:
rails db:migrate
That’s it. Technically our authentication system is set up. Now we can use Devise given methods and create new users. Commit the change:
而已。 从技术上讲,我们建立了身份验证系统。 现在,我们可以使用Devise给定的方法并创建新用户。 提交更改:
git add -A
git commit -m "Add and configure the Devise gem"
By installing Devise gem, we not only get the back-end functionality, but also default views. If you list your routes by running:
通过安装Devise gem,我们不仅获得了后端功能,而且还获得了默认视图。 如果通过运行列出路线:
rails routes
You can see that now you have a bunch of new routes. Remember, we only had a one root route until now. If something seems to be confusing, you can always open devise docs and get your answers. Also don’t forget that a lot of same questions come to other people’ s minds. There’s a high chance that you’ll find the answer by Googling too.
您可以看到,现在有了许多新路线。 记住,到目前为止,我们只有一条根本路线。 如果看起来有些混乱,您可以随时打开devise文档并获取答案。 同样不要忘记,其他人也会想到很多相同的问题。 您很有可能也可以通过Google搜索找到答案。
Try some of those routes. Go to localhost:3000/users/sign_in and you should see a sign in page.
尝试其中一些路线。 转到localhost:3000 / users / sign_in ,您应该会看到一个登录页面。
If you went to localhost:3000/users/sign_up, you would see a sign up page too. God Damn! as Noob Noob says. If you look at the views
directory, you see that there isn’t any Devise directory, which we could modify. As Devise docs says, in order to modify Devise views, we’ve to generate it with a devise generator. Run
如果您访问localhost:3000 / users / sign_up ,那么也会看到一个注册页面。 天哪! 正如Noob Noob所说。 如果查看views
目录,则会发现没有任何Devise目录,我们可以对其进行修改。 如Devise的文档所述,为了修改Devise视图,我们必须使用devise生成器来生成它。 跑
rails generate devise:views
If you check the views
directory, you’ll see a generated devise directory inside. Here we can modify how sign up and login pages are going to look like. Let’s start with the login page, because in our case this is going to be a more straightforward implementation. With the registration page, due to our wanted feature, an extra effort will be required.
如果查看views
目录,则会在其中看到一个生成的devise目录。 在这里,我们可以修改注册和登录页面的外观。 让我们从登录页面开始,因为在我们的例子中,这将是一个更直接的实现。 在注册页面上,由于我们想要的功能,将需要付出额外的努力。
Login page
登录页面
Navigate to and open app/views/devise/sessions/new.html.erb
.
导航到并打开app/views/devise/sessions/new.html.erb
。
This is where the login page views are stored. There’s just a login form inside the file. As you may have noticed, the form_for
method is used to generate this form. This is a handy Rails method to generate forms. We’re going to modify this form’s style with bootstrap. Replace all file’s content with:
这是登录页面视图的存储位置。 文件内只有一个登录表单。 您可能已经注意到, form_for
方法用于生成此表单。 这是生成表单的便捷的Rails方法。 我们将使用引导程序修改此表单的样式。 将所有文件的内容替换为:
Nothing fancy is going here. We just modified this form to be a bootstrap form by changing the method’s name to bootstrap_form_for
and adding form-control
classes to the fields.
这里没有幻想。 通过将方法的名称更改为bootstrap_form_for
并将form-control
类添加到字段中,我们刚刚将该表单修改为引导表单。
Take a look how arguments inside the methods are styled. Every argument starts in a new line. The reason why I did this is to avoid having long code lines. Usually code lines shouldn’t be longer than 80 characters, it improves readability. We’re going to style the code like that for the rest of the guide.
看一下方法内部参数的样式。 每个参数都以新行开头。 我这样做的原因是为了避免使用较长的代码行。 通常,代码行的长度不得超过80个字符,这样可以提高可读性。 在本指南的其余部分中,我们将对代码进行样式设置。
If we visit localhost:3000/users/sign_in, we’ll see that it gives us an error:
如果我们访问localhost:3000 / users / sign_in ,则会看到它给我们一个错误:
undefined method 'bootstrap_form_for'
In order to use bootstrap forms in Rails we’ve to add a bootstrap_form gem. Add this to the Gemfile
为了在Rails中使用引导程序表单,我们必须添加一个bootstrap_form gem。 将此添加到Gemfile
gem 'bootstrap_form'
Then run:
然后运行:
bundle install
At this moment the login page should look like this:
此时,登录页面应如下所示:
Commit changes:
提交更改:
git add -A
git commit -m "Generate devise views, modify sign in form
and add the bootstrap_form gem."
To give the bootstrap’s grid system to the page, wrap login form with the bootstrap container.
要将引导程序的网格系统提供给页面,请使用引导程序容器包装登录表单。
The width of the login form is 6 columns out of 12. And the offset is 3 columns. On smaller devices the form will take full screen’s width. That’s how the bootstrap gridworks.
登录表单的宽度为12列中的6列,而偏移量为3列。 在较小的设备上,表格将采用全屏宽度。 引导网格就是这样工作的。
Let’s do another commit. Quite a minor change, huh? But that’s how I usually do commits. I implement a definite change in one area and then commit it. I think doing it this way helps to track changes and understand how the code has evolved.
让我们再次提交。 很小的变化,是吗? 但这就是我通常的提交方式。 我在一个区域中进行了确定的更改,然后提交了。 我认为以这种方式进行操作有助于跟踪更改并了解代码如何演变。
git add -A
git commit -m "wrap login form in the login page with a boostrap container"
It would be better if we could just reach the login page by going to /login
instead of /users/sign_in
. We have to change the route. To do that we need to know where the action, which gets called when we go to login page, is located. Devise controllers are located inside the gem itself. By reading Devise docs we can see that all controllers are located inside the devise
directory. Not really surprised by the discovery, to be honest U_U. By using the devise_scope
method we can simply change the route. Go to routes.rb
file and add
如果我们可以直接通过/login
而不是/users/sign_in
来访问登录页面, /users/sign_in
。 我们必须改变路线。 为此,我们需要知道该操作的位置,该操作在登录页面时被调用。 设计控制器位于gem本身内部。 通过阅读Devise文档,我们可以看到所有控制器都位于devise
目录中。 说实话,U_U并不为这一发现感到惊讶。 通过使用devise_scope
方法,我们可以简单地更改路线。 转到routes.rb
文件并添加
devise_scope :user do
get 'login', to: 'devise/sessions#new'
end
Commit the change:
提交更改:
git add -A
git commit -m "change route from /users/sign_in to /login"
For now, leave the login page as it is.
现在,保持登录页面不变。
Sign up page
注册页面
If we navigated to localhost:3000/users/sign_up, we would see the default Devise sign up page. But as mentioned above, the sign up page will require some extra effort. Why? Because we want to add a new :name
column to the users
table, so a User object could have the :name
attribute.
如果导航到localhost:3000 / users / sign_up ,则会看到默认的Devise注册页面。 但是如上所述,注册页面将需要一些额外的努力。 为什么? 因为我们要向users
表添加新的:name
列,所以User对象可以具有:name
属性。
We’re about to do some changes to the schema.rb
file. At this moment, if you aren’t quite familiar with schema changes and migrations, I recommend you to read through Active Record Migrations docs.
我们将对schema.rb
文件进行一些更改。 目前,如果您不太熟悉架构更改和迁移,建议您通读Active Record Migrations文档。
First, we have to add an extra column to the users
table. We could create a new migration file and use a change_table
method to add an extra column. But we are just at the development stage, our app isn’t deployed yet. We can just define a new column straight inside the devise_create_users
migration file and then recreate the database. Navigate to db/migrate
and open the *CREATION_DATE*_devise_create_users.rb
file and add t.string :name, null: false, default: ""
inside the create_table
method.
首先,我们必须在users
表中添加一个额外的列。 我们可以创建一个新的迁移文件,并使用change_table
方法添加额外的列。 但是我们正处于开发阶段,尚未部署我们的应用程序。 我们可以直接在devise_create_users
迁移文件中定义一个新列,然后重新创建数据库。 导航到db/migrate
并打开*CREATION_DATE*_devise_create_users.rb
文件,然后在create_table
方法内添加t.string :name, null: false, default: ""
。
Now run the commands to drop and create the database, and run migrations.
现在,运行命令以删除和创建数据库,并运行迁移。
rails db:drop
rails db:create
rails db:migrate
We added a new column to the users table and altered the schema.rb
file.
我们向用户表添加了新列,并更改了schema.rb
文件。
To be able to send an extra attribute, so the Devise controller would accept it, we’ve to do some changes at the controller level. We can do changes to Devise controllers in few different ways. We can use devise generator and generate controllers. Or we can create a new file, specify the controller and the methods that we want to modify. Both ways are good. We are going to use the latter one.
为了能够发送额外的属性,以便Devise控制器可以接受它,我们必须在控制器级别进行一些更改。 我们可以通过几种不同的方式对Devise控制器进行更改。 我们可以使用devise生成器并生成控制器。 或者,我们可以创建一个新文件,指定我们要修改的控制器和方法。 两种方式都很好。 我们将使用后一种。
Navigate to app/controllers
and create a new file registrations_controller.rb
. Add the following code to the file:
导航到app/controllers
并创建一个新文件registrations_controller.rb
。 将以下代码添加到文件中:
This code overwrites the sign_up_params
and account_update_params
methods to accept the :name
attribute. As you see, those methods are in the Devise RegistrationsController
, so we specified it and altered its methods. Now inside our routes we have to specify this controller, so these methods could be overwritten. Inside routes.rb
change
此代码覆盖sign_up_params
和account_update_params
方法以接受:name
属性。 如您所见,这些方法在Devise RegistrationsController
,因此我们指定了它并更改了它的方法。 现在,我们必须在路由内指定此控制器,以便可以覆盖这些方法。 内部routes.rb
更改
devise_for :users
to
至
devise_for :users, :controllers => {:registrations => "registrations"}
Commit the changes.
提交更改。
git add -A
git commit -m "
- Add the name column to the users table.
- Include name attribute to sign_up_params and account_update_params
methods inside the RegistrationsController"
Open the new.html.erb
file:
打开new.html.erb
文件:
app/views/devise/registrations/new.html.erb
Again, remove everything except the form. Convert the form into a bootstrap form. This time we add an additional name field.
同样,删除除表格外的所有内容。 将表单转换为引导表单。 这次,我们添加了一个附加名称字段。
Commit the change.
提交更改。
git add -A
git commit -m "
Delete everything from the signup page, except the form.
Convert form into a bootstrap form. Add an additional name field"
Wrap the form with a bootstrap container and add some text.
用引导容器包装表格,并添加一些文本。
Commit the change.
提交更改。
git add -A
git commit -m "
Wrap the sign up form with a bootstrap container.
Add informational text inside the container"
Just like with the login page, it would be better if we could just open a sign up page by going to /signup
instead of users/sign_up
. Inside the routes.rb
file add the following code:
就像登录页面一样,最好通过转到/signup
而不是users/sign_up
来打开注册页面。 在routes.rb
文件中,添加以下代码:
devise_scope :user do
get 'signup', to: 'devise/registrations#new'
end
Commit the change.
提交更改。
git add -A
git commit -m "Change sign up page's route from /users/sign_up to /signup"
Let’s apply a few style changes before we move on. Navigate to app/assets/sytlesheets/partials
and create a new signup.scss
file. Inside the file add the following CSS:
在继续之前,让我们应用一些样式更改。 导航到app/assets/sytlesheets/partials
并创建一个新的signup.scss
文件。 在文件内部添加以下CSS:
Also we haven’t imported files from the partials
directory inside the application.scss
file. Let’s do it right now. Navigate to the application.scss
and just above the @import partials/layout/*
, import all files from the partials
directory. Application.scss
should look like this
另外,我们还没有从application.scss
文件内的partials
目录中导入文件。 现在就开始吧。 导航到application.scss
并在@import partials/layout/*
上方,从partials
目录导入所有文件。 Application.scss
应该看起来像这样
Commit the changes.
提交更改。
git add -A
git commit -m "
- Create a signup.scss and add CSS to the sign up page
- Import all files from partials directory to the application.scss"
Add few other style changes to the overall website look. Navigate to app/assets/stylesheets/base
and create a new default.scss
file. Inside the file add the following CSS code:
在整体网站外观中添加其他样式更改。 导航到app/assets/stylesheets/base
并创建一个新的default.scss
文件。 在文件内部添加以下CSS代码:
Here we apply some general style changes for the whole website. font-size
is set to 62.5%
, so 1 rem
unit could represent 10px
. If you don’t know what the rem unit is, you may want to read this tutorial. We don’t want to see a label text on bootstrap forms, that’s why we set this:
在这里,我们对整个网站进行了一些常规样式更改。 font-size
设置为62.5%
,因此1 rem
单位可以表示10px
。 如果您不知道rem单元是什么,则可能需要阅读本教程 。 我们不想在引导表单上看到标签文本,这就是为什么要设置它:
.control-label {
display: none;
}
You may have noticed that the $backgroundColor
variable is used. But this variable isn’t set yet. So let’s do it by opening variables.scss
file and adding this:
您可能已经注意到使用了$backgroundColor
变量。 但是尚未设置此变量。 因此,通过打开variables.scss
文件并添加以下内容来完成此操作:
$backgroundColor: #f0f0f0;
The default.scss
file isn’t imported inside the application.scss
. Import it below variables, the application.scss
file should look like this:
没有将default.scss
文件导入application.scss
内部。 将其导入变量下面, application.scss
文件应如下所示:
Commit the changes.
提交更改。
git add -A
git commit -m "
Add CSS and import CSS files inside the main file
- Create a default.scss file and add CSS
- Define $backgroundColor variable
- Import default.scss file inside the application.scss"
Navigation bar update
导航栏更新
Right now we have three different pages: home, login and signup. It is a good idea to connect them all together, so users could navigate through the website effortlessly. We’ll put links to signup and login pages on the navigation bar. Navigate to and open the _navigation.html.erb
file.
现在,我们有三个不同的页面:主页,登录和注册。 将它们连接在一起是个好主意,这样用户就可以轻松浏览网站。 我们将在导航栏中放置指向注册和登录页面的链接。 导航到并打开_navigation.html.erb
文件。
app/views/layouts/_navigation.html.erb
We’re going to add some extra code here. In the future we will add even more code here. This will lead to a file with lots of code, which is hard to manage and test. In order to handle a long code easier, we’re going to start splitting larger code into smaller chunks. To achieve that, we’ll use partials. Before adding extra code, let’s split the current _navigation.html.erb
code into partials already.
我们将在此处添加一些额外的代码。 将来,我们将在此处添加更多代码。 这将导致文件包含很多代码,这些代码很难管理和测试。 为了更轻松地处理长代码,我们将开始将较大的代码拆分为较小的块。 为此,我们将使用局部函数。 在添加额外的代码之前,让我们_navigation.html.erb
当前的_navigation.html.erb
代码拆分为部分代码。
Let me quickly introduce you how our navigation bar is going to work. We’ll have two major parts. On one part elements will be shown all the time, no matter what the screen size is. On the other part of the navigation bar, elements will be shown only on bigger screens and collapsed on the smaller ones.
让我快速向您介绍我们的导航栏如何工作。 我们将分为两个主要部分。 无论屏幕大小如何,元素都会一直显示在一个部分上。 在导航栏的另一部分,元素仅在较大的屏幕上显示,而在较小的屏幕上折叠。
This is how the structure inside the .container
element will look like:
.container
元素内部的结构如下所示:
Inside the layouts directory:
在layouts目录中:
app/views/layouts
Create a new navigation
directory. Inside this directory create a new partial _header.html.erb
file.
创建一个新的navigation
目录。 在此目录中,创建一个新的_header.html.erb
部分文件。
app/views/layouts/navigation/_header.html.erb
From the _navigation.html.erb
file cut the whole .navbar-header
section and paste it inside the _header.html.erb
file. Inside the navigation
directory, create another partial file named _collapsible_elements.html.erb
.
从_navigation.html.erb
文件中剪切整个.navbar-header
部分,并将其粘贴到_header.html.erb
文件中。 在navigation
目录中,创建另一个名为_collapsible_elements.html.erb
部分文件。
app/views/layouts/navigation/_collapsible_elements.html.erb
From the _navigation.html.erb
file cut the whole .navbar-collapse
section and paste it inside the _collapsible_elements.html.erb
. Now let’s render those two partials inside the _navigation.html.erb
file. The file should look like this now.
从_navigation.html.erb
文件中剪切整个.navbar-collapse
部分,并将其粘贴到_collapsible_elements.html.erb
。 现在,让我们在_navigation.html.erb
文件中呈现这两个部分。 该文件现在应该看起来像这样。
If you went to http://localhost:3000 now, you wouldn’t notice any difference. We just cleaned our code a little bit and prepared it for a further development.
如果您现在访问http:// localhost:3000 ,则不会有任何区别。 我们只是稍微清理了一下代码,并将其准备好进行进一步的开发。
We are ready to add some links to the navigation bar. Navigate to and open the _collapsible_elements.html.erb
file again:
我们准备将一些链接添加到导航栏。 导航至并再次打开_collapsible_elements.html.erb
文件:
app/views/layouts/_collapsible_elements.html.erb
Let’s fill this file with links, replace the file’s content with:
让我们用链接填充该文件,将文件内容替换为:
Let me briefly explain to you what is going on here. First, at the second line I changed the element’s id
to navbar-collapsible-content
. This is required in order to make this content collapsible. It’s a bootstrap’s functionality. The default id
was bs-example-navbar-collapse-1
. To trigger this this function there’s the button with the data-target
attribute inside the _header.html
file. Open views/layouts/navigation/_header.html.erb
and change data-target
attribute to data-target="#navbar-collapsible-content"
. Now the button will trigger the collapsible content.
让我简要地向您解释这里发生了什么。 首先,在第二行中,将元素的id
更改为navbar-collapsible-content
。 为了使此内容可折叠,这是必需的。 它是引导程序的功能。 默认id
为bs-example-navbar-collapse-1
。 要触发此功能,请在_header.html
文件中使用带有data-target
属性的按钮。 打开views/layouts/navigation/_header.html.erb
并将data-target
属性更改为data-target="#navbar-collapsible-content"
。 现在,该按钮将触发可折叠的内容。
Next, inside the_collapsible_elements.html.erb
file you see some if else
logic with the user_signed_in?
Devise method. This will show different links based on if a user is signed in, or not. Leaving logic, such as if else
statements inside views isn’t a good practice. Views should be pretty “dumb” and just spit the information out, without “thinking” at all. We will refactor this logic later with Helpers.
接下来,在_collapsible_elements.html.erb
文件中,您可以看到_collapsible_elements.html.erb
if else
逻辑user_signed_in?
设计方法。 根据用户是否登录,这将显示不同的链接。 留下逻辑,例如在视图中if else
语句不是一个好习惯。 视图应该是相当“愚蠢”的,只是将信息吐出来,根本没有“思考”。 稍后,我们将与Helpers一起重构此逻辑。
The last thing to note inside the file is pc-menu
and mobile-menu
CSS classes. The purpose of these classes is to control how links are displayed on different screen sizes. Let’s add some CSS for these classes. Navigate to app/assets/stylesheets
and create a new directory responsive
. Inside the directory create two files, desktop.scss
and mobile.scss
. The purpose of those files is to have different configurations for different screen sizes. Inside the desktop.scss
file add:
文件中要注意的最后一件事是pc-menu
和mobile-menu
CSS类。 这些类的目的是控制如何在不同的屏幕尺寸上显示链接。 让我们为这些类添加一些CSS。 导航到app/assets/stylesheets
并创建一个新的responsive
目录。 在目录内创建两个文件, desktop.scss
和mobile.scss
。 这些文件的目的是针对不同的屏幕尺寸具有不同的配置。 在desktop.scss
文件中添加:
Inside the mobile.scss
file add:
在mobile.scss
文件中添加:
If you aren’t familiar with CSS media queries, read this. Import files from the responsive
directory inside the application.scss
file. Import it at the bottom of the file, so the application.scss
should look like this:
如果您不熟悉CSS媒体查询,请阅读this 。 从application.scss
文件内的responsive
目录导入文件。 将其导入文件的底部,因此application.scss
应该如下所示:
Navigate to and open navigation.scss
file
导航到并打开navigation.scss
文件
app/assets/stylesheets/partials/layout/navigation.scss
and do some stylistic tweaks to the navigation bar by adding the following inside the nav
element’s selector:
并通过在nav
元素的选择器内添加以下内容,对导航栏进行一些样式调整:
And outside the nav
element, add the following CSS code:
在nav
元素之外,添加以下CSS代码:
At this moment, our application should look like this when a user is not logged in:
此时,当用户未登录时,我们的应用程序应如下所示:
Like this when a user is logged in:
用户登录后,像这样:
And like this when the screen size is smaller:
当屏幕较小时,如下所示:
Commit the changes.
提交更改。
git add -A
git commit -m "
Update the navigation bar
- Add login, signup, logout and edit profile links on the navigation bar
- Split _navigation.scss code into partials
- Create responsive directory inside the stylesheets directory and add CSS.
- Add CSS to tweak navigation bar style"
Now we have a basic authentication functionality. It satisfies our needs. So let’s merge authentication
branch with the master
branch.
现在,我们有了基本的身份验证功能。 它满足了我们的需求。 因此,让我们将authentication
分支与master
分支合并。
git checkout master
git merge authentication
We can see the summary of changes again. Authentication branch is not needed anymore, so delete it.
我们可以再次看到更改摘要。 不再需要身份验证分支,因此将其删除。
git branch -D authentication
帮手 (Helpers)
When we were working on the _collapsible_elements.html.erb
file, I mentioned that Rails views is not the right place for logic. If you look inside the app
directory of the project, you see there’s the directory called helpers
. We’ll extract logic from Rails views and put it inside the helpers
directory.
在处理_collapsible_elements.html.erb
文件时,我提到过Rails视图不是逻辑的正确位置。 如果在项目的app
目录中查看,就会看到一个名为helpers
的目录。 我们将从Rails视图中提取逻辑,并将其放在helpers
目录中。
app/views/pages
Let’s create our first helpers. Firstly, create a new branch and switch to it.
让我们创建我们的第一个助手。 首先,创建一个新分支并切换到该分支。
git checkout -B helpers
Navigate to the helpers
directory and create a new navigation_helper.rb
file
导航到helpers
目录并创建一个新的navigation_helper.rb
文件
app/helpers/navigation_helper.rb
Inside helper files, helpers are defined as modules. Inside the navigation_helper.rb
define the module.
在帮助程序文件中,帮助程序定义为modules 。 在navigation_helper.rb
内部定义模块。
By default Rails loads all helper files to all views. Personally I do not like this, because methods’ names from different helper files might clash. To override this default behavior open the application.rb
file
默认情况下,Rails将所有帮助程序文件加载到所有视图。 我个人不喜欢这样,因为来自不同帮助文件的方法名称可能会冲突。 要覆盖此默认行为,请打开application.rb
文件
config/application.rb
Inside the Application
class add this configuration
在Application
类内部添加此配置
config.action_controller.include_all_helpers = false
Now helpers are available for corresponding controller’s views only. So if we have the PagesController
, all helpers inside the pages_helper.rb
file will be available to all view files inside the pages
directory.
现在,助手仅可用于相应控制器的视图。 因此,如果我们拥有PagesController
,则pages_helper.rb
文件中的所有帮助器将可用于pages
目录中的所有视图文件。
We don’t have the NavigationController
, so helper methods defined inside the NavigationHelper
module won’t be available anywhere. The navigation bar is available across the whole website. We can include the NavigationHelper
module inside the ApplicationHelper
. If you aren’t familiar with loading and including files, read through this article to get an idea what is going to happen.
我们没有NavigationController
,因此NavigationHelper
模块中定义的helper方法将无法在任何地方使用。 导航栏可在整个网站上使用。 我们可以在ApplicationHelper
包含NavigationHelper
模块。 如果您不熟悉加载和包含文件,请通读本文以了解将要发生的事情。
Inside the application_helper.rb
file, require the navigation_helper.rb
file. Now we have an access to the navigation_helper.rb
file’s content. So let’s inject NavigationHelper
module inside the ApplicationHelper
module by using an include
method. The application_helper.rb
should look like this:
在application_helper.rb
文件中,需要navigation_helper.rb
文件。 现在,我们可以访问navigation_helper.rb
文件的内容。 So let's inject NavigationHelper
module inside the ApplicationHelper
module by using an include
method. The application_helper.rb
should look like this:
Now NavigationHelper
helper methods are available across the whole app.
Now NavigationHelper
helper methods are available across the whole app.
Navigate to and open the _collapsible_elements.html.erb
file
Navigate to and open the _collapsible_elements.html.erb
file
app/views/layouts/navigation/_collapsible_elements.html.erb
We’re going to split the content inside the if else
statements into partials. Create a new collapsible_elements
directory inside the navigation
directory.
We're going to split the content inside the if else
statements into partials. Create a new collapsible_elements
directory inside the navigation
directory.
app/views/layouts/navigation/collapsible_elements
Inside the directory create two files: _signed_in_links.html.erb
and _non_signed_in_links.html.erb
. Now cut the content from _collapsible_elements.html.erb
file’s if else
statements and paste it to the corresponding partials. The partials should look like this:
Inside the directory create two files: _signed_in_links.html.erb
and _non_signed_in_links.html.erb
. Now cut the content from _collapsible_elements.html.erb
file's if else
statements and paste it to the corresponding partials. The partials should look like this:
Now inside the _collapsible_elements.html.erb
file, instead of if else
statements, add therender
method with the collapsible_links_partial_path
helper method as an argument. The file should look like this
Now inside the _collapsible_elements.html.erb
file, instead of if else
statements, add the render
method with the collapsible_links_partial_path
helper method as an argument. The file should look like this
collapsible_links_partial_path
is the method we are going to define inside the NavigationHelper
. Open navigation_helper.rb
collapsible_links_partial_path
is the method we are going to define inside the NavigationHelper
. Open navigation_helper.rb
app/helpers/navigation_helper.rb
and define the method inside the module. The navigation_helper.rb
file should look like this:
and define the method inside the module. The navigation_helper.rb
file should look like this:
The defined method is pretty straightforward. If a user is signed in, return a corresponding partial’s path. If a user is not signed in, return another partial’s path.
The defined method is pretty straightforward. If a user is signed in, return a corresponding partial's path. If a user is not signed in, return another partial's path.
We’ve created our first helper method and extracted logic from views to a helper method. We’re going to do this for the rest of the guide, whenever we encounter logic inside a view file. By doing this we’re making a favor to ourselves, testing and managing the app becomes much easier.
We've created our first helper method and extracted logic from views to a helper method. We're going to do this for the rest of the guide, whenever we encounter logic inside a view file. By doing this we're making a favor to ourselves, testing and managing the app becomes much easier.
The app should look and function the same.
The app should look and function the same.
Commit the changes.
Commit the changes.
git add -A
git commit -m "Configure and create helpers
- Change include_all_helpers config to false
- Split the _collapsible_elements.html.erb file's content into
partials and extract logic from the file into partials"
Merge the helpers
branch with the master
Merge the helpers
branch with the master
git checkout master
git merge helpershttps://gist.github.com/domagude/419bba70cb97e27f4ea04fe37820194a#file-rails_helper-rb
测试中 (Testing)
At this point the application has some functionality. Even thought there aren’t many features yet, but we already have to spend some time by manually testing the app if we want to make sure that everything works. Imagine if the application had 20 times more features than it has now. What a frustration would be to check that everything works fine, every time we did code changes. To avoid this frustration and hours of manual testing, we’ll implement automated tests.
At this point the application has some functionality. Even thought there aren't many features yet, but we already have to spend some time by manually testing the app if we want to make sure that everything works. Imagine if the application had 20 times more features than it has now. What a frustration would be to check that everything works fine, every time we did code changes. To avoid this frustration and hours of manual testing, we'll implement automated tests .
Before diving into tests writing, allow me to introduce you how and what I test. Also you can read through A Guide to Testing Rails Applications to get familiar with default Rails testing techniques.
Before diving into tests writing, allow me to introduce you how and what I test. Also you can read through A Guide to Testing Rails Applications to get familiar with default Rails testing techniques.
What I use for testing
What I use for testing
Framework: RSpec When I started testing my Rails apps, I used the default Minitestframework. Now I use RSpec. I don’t think there’s a good or a bad choice here. Both frameworks are great. I think it depends on a personal preference, which framework to use. I’ve heard that RSpec is a popular choice among Rails community, so I’ve decided to give it a shot. Now I am using it most of the time.
Framework: RSpec When I started testing my Rails apps, I used the default Minitest framework. Now I use RSpec. I don't think there's a good or a bad choice here. Both frameworks are great. I think it depends on a personal preference, which framework to use. I've heard that RSpec is a popular choice among Rails community, so I've decided to give it a shot. Now I am using it most of the time.
Sample data: factory_girl Again, at first I tried the default Rails way — fixtures, to add sample data. I’ve found that it’s a different case than it is with testing frameworks. Which testing framework to choose is probably a personal preference. In my opinion it’s not the case with sample data. At first fixtures were fine. But I’ve noticed that after apps become larger, controlling sample data with fixtures becomes tough. Maybe I used it in a wrong way. But with factories everything was nice and peaceful right away. No matter if an app is smaller or bigger — the effort to set sample data is the same.
Sample data: factory_girl Again, at first I tried the default Rails way — fixtures , to add sample data. I've found that it's a different case than it is with testing frameworks. Which testing framework to choose is probably a personal preference. In my opinion it's not the case with sample data. At first fixtures were fine. But I've noticed that after apps become larger, controlling sample data with fixtures becomes tough. Maybe I used it in a wrong way. But with factories everything was nice and peaceful right away. No matter if an app is smaller or bigger — the effort to set sample data is the same.
Acceptance tests: Capybara By default Capybara uses rack_test driver. Unfortunately, this driver doesn’t support JavaScript. Instead of the default Capybara’s driver, I chose to use poltergeist. It supports JavaScript and in my case it was the easiest driver to set up.
Acceptance tests: Capybara By default Capybara uses rack_test driver. Unfortunately, this driver doesn't support JavaScript. Instead of the default Capybara's driver, I chose to use poltergeist . It supports JavaScript and in my case it was the easiest driver to set up.
What I test
What I test
I test all logic which is written by me. It could be:
I test all logic which is written by me. 它可能是:
- Helpers 帮手
- Models 楷模
- Jobs 工作
- Design Patterns 设计模式
- Any other logic written by me Any other logic written by me
Besides logic, I wrap my app with acceptance tests using Capybara, to make sure that all app’s features are working properly by simulating a user’s interaction. Also to help my simulation tests, I use request tests to make sure that all requests return correct responses.
Besides logic, I wrap my app with acceptance tests using Capybara, to make sure that all app's features are working properly by simulating a user's interaction. Also to help my simulation tests, I use request tests to make sure that all requests return correct responses.
That’s what I test in my personal apps, because it fully satisfies my needs. Obviously, testing standards could be different from person to person and from company to company.
That's what I test in my personal apps, because it fully satisfies my needs. Obviously, testing standards could be different from person to person and from company to company.
Controllers, views and gems weren’t mentioned, why? As many Rails developers say, controllers and views shouldn’t contain any logic. And I agree with them. In this case there isn’t much to test then. In my opinion, user simulation tests are enough and efficient for views and controllers. And gems are already tested by their creators. So I think that simulation tests are enough to make sure that gems work properly too.
Controllers, views and gems weren't mentioned, why? As many Rails developers say, controllers and views shouldn't contain any logic. And I agree with them. In this case there isn't much to test then. In my opinion, user simulation tests are enough and efficient for views and controllers. And gems are already tested by their creators. So I think that simulation tests are enough to make sure that gems work properly too.
How I test
How I test
Of course I try to use TDD approach whenever is possible. Write a test first and then implement the code. In this case the development flow becomes more smoother. But sometimes you aren’t sure how the completed feature is going to look like and what kind of output to expect. You might be experimenting with the code or just trying different implementation solutions. So in those cases, test first and implementation later approach doesn’t really work.
Of course I try to use TDD approach whenever is possible. Write a test first and then implement the code. In this case the development flow becomes more smoother. But sometimes you aren't sure how the completed feature is going to look like and what kind of output to expect. You might be experimenting with the code or just trying different implementation solutions. So in those cases, test first and implementation later approach doesn't really work.
Before (sometimes after, as discussed above) every piece of logic I write, I write an isolated test for it a.k.a. unit test. To make sure that every feature of an app works, I write acceptance (user simulation) tests with Capybara.
Before (sometimes after, as discussed above) every piece of logic I write, I write an isolated test for it aka unit test . To make sure that every feature of an app works, I write acceptance (user simulation) tests with Capybara.
Set up a test environment
Set up a test environment
Before we write our first tests, we have to configure the testing environment.
Before we write our first tests, we have to configure the testing environment.
Open the Gemfile
and add those gems to the test group
Open the Gemfile
and add those gems to the test group
gem 'rspec-rails', '~> 3.6'
gem 'factory_girl_rails'
gem 'rails-controller-testing'
gem 'headless'
gem 'capybara'
gem 'poltergeist'
gem 'database_cleaner'
As discussed above, rspec
gem is a testing framework, factory_girl
is for adding sample data, capybara
is for simulating a user’s interaction with the app and poltergeist
driver gives the JavaScript support for your tests.
As discussed above, rspec
gem is a testing framework, factory_girl
is for adding sample data, capybara
is for simulating a user's interaction with the app and poltergeist
driver gives the JavaScript support for your tests.
You can use another driver which supports JavaScript if it’s easier for you to set up. If you decide to use poltergeist
gem, you will need PhantomJS installed. To install PhantomJS read poltergeist docs.
You can use another driver which supports JavaScript if it's easier for you to set up. If you decide to use poltergeist
gem, you will need PhantomJS installed. To install PhantomJS read poltergeist docs .
headless
gem is required to support headless drivers. poltergeist
is a headless driver, that’s why we need this gem. rails-controller-testing
gem is going to be required when we will test requests and responses with the requests specs. More on that later.
headless
gem is required to support headless drivers. poltergeist
is a headless driver, that's why we need this gem. rails-controller-testing
gem is going to be required when we will test requests and responses with the requests specs . 以后再说。
database_cleaner
is required to clean the test database after tests where JavaScript was executed. Normally the test database cleans itself after each test, but when you test features which has some JavaScript, the database doesn’t clean itself automatically. It might change in the future, but at the moment, of writing this tutorial, after tests with JavaScript are executed, the test database isn’t cleaned automatically. That’s why we have to manually configure our test environment to clean the test database after each JavaScript test too. We’ll configure when to run the database_cleaner
gem in just a moment.
database_cleaner
is required to clean the test database after tests where JavaScript was executed. Normally the test database cleans itself after each test, but when you test features which has some JavaScript, the database doesn't clean itself automatically. It might change in the future, but at the moment, of writing this tutorial, after tests with JavaScript are executed, the test database isn't cleaned automatically. That's why we have to manually configure our test environment to clean the test database after each JavaScript test too. We'll configure when to run the database_cleaner
gem in just a moment.
Now when the purpose of these gems is covered, let’s install them by running:
Now when the purpose of these gems is covered, let's install them by running:
bundle install
To initialize the spec
directory for the RSpec framework run the following:
To initialize the spec
directory for the RSpec framework run the following:
rails generate rspec:install
Generally speaking, spec means a single test in RSpec framework. When we run our specs, it means that we run our tests.
Generally speaking, spec means a single test in RSpec framework. When we run our specs, it means that we run our tests.
If you look inside the app
directory, you will notice a new directory called spec
. This is where we’re going to write tests. Also you may have noticed a directory called test
. This is where tests are stored when you use a default testing configuration. We won’t use this directory at all. You can simply remove it from the project c(x_X)b.
If you look inside the app
directory, you will notice a new directory called spec
. This is where we're going to write tests. Also you may have noticed a directory called test
. This is where tests are stored when you use a default testing configuration. We won't use this directory at all. You can simply remove it from the project c(x_X)b.
As mentioned above, we have to set up the database_cleaner
for the tests which include JavaScript. Open the rails_helper.rb
file
As mentioned above, we have to set up the database_cleaner
for the tests which include JavaScript. Open the rails_helper.rb
file
spec/rails_helper.rb
Change this line
Change this line
config.use_transactional_fixtures = true
to
至
config.use_transactional_fixtures = false
and below it add the following code:
and below it add the following code:
I took this code snippet from this tutorial.
I took this code snippet from this tutorial .
The last thing we’ve to do is to add some configurations. Inside the rails_helper.rb
file’s configurations, add the following lines
The last thing we've to do is to add some configurations. Inside the rails_helper.rb
file's configurations, add the following lines
Let’s breakdown the code a little bit.
Let's breakdown the code a little bit.
With require
methods we load files from the new added gems, so we could use their methods below.
With require
methods we load files from the new added gems, so we could use their methods below.
config.include Devise::Test::IntegrationHelpers, type: :feature
This configuration allows us to use devise
methods inside capybara
tests. How did I come up with this line? It was provided inside the Devise docs.
This configuration allows us to use devise
methods inside capybara
tests. How did I come up with this line? It was provided inside the Devise docs .
config.include FactoryGirl::Syntax::Methods
This configuration allows to use factory_girl
gem’s methods. Again, I found this configuration inside the gem’s documentation.
This configuration allows to use factory_girl
gem's methods. Again, I found this configuration inside the gem's documentation.
Capybara.javascript_driver = :poltergeist
Capybara.server = :puma
Those two configurations are required in order to be able to test JavaScript with capybara
. Always read the documentation first, when you want to implement something you don’t know how to.
Those two configurations are required in order to be able to test JavaScript with capybara
. Always read the documentation first, when you want to implement something you don't know how to.
The reason why I introduced you with most of the testing gems and configurations at once and not gradually, once we meet a particular problem, is to give you a clear picture what I use for testing. Now you can always come back to this section and check majority of the configurations in one place. Rather than jumping from one place to another and putting gems with configurations like puzzle pieces together.
The reason why I introduced you with most of the testing gems and configurations at once and not gradually, once we meet a particular problem, is to give you a clear picture what I use for testing. Now you can always come back to this section and check majority of the configurations in one place. Rather than jumping from one place to another and putting gems with configurations like puzzle pieces together.
Let’s commit the changes and finally get our hands dirty with tests.
Let's commit the changes and finally get our hands dirty with tests.
git add -A
git commit -m "
Set up the testing environment
- Remove test directory
- Add and configure rspec-rails, factory_girl_rails,
rails-controller-testing, headless, capybara, poltergeist,
database_cleaner gems"
Helper specs
Helper specs
About each type of specs (tests), you can find general information by reading rspec docs and its gem docs. Both are pretty similar, but you can find some differences between each other.
About each type of specs (tests), you can find general information by reading rspec docs and its gem docs . Both are pretty similar, but you can find some differences between each other.
Create and switch to a new branch:
Create and switch to a new branch:
git checkout -b specs
So far we’ve created only one helper method. Let’s test it.
So far we've created only one helper method. 让我们测试一下。
Navigate to spec
directory anhttps://gist.github.com/domagude/3c42ba6ccf31bf1c50588c59277a9146#file-navigation_helper_spec-rbd create a new directory called helpers
.
Navigate to spec
directory an https://gist.github.com/domagude/3c42ba6ccf31bf1c50588c59277a9146#file-navigation_helper_spec-rb d create a new directory called helpers
.
spec/helpers
Inside the directory, create a new file navigation_helper_spec.rb
Inside the directory, create a new file navigation_helper_spec.rb
spec/helpers/navigation_helper_spec.rb
Inside the file, write the following code:
Inside the file, write the following code:
require ‘rails_helper'
gives us an access to all testing configurations and methods. :type => :helper
treats our tests as helper specs and provides us with specific methods.
require 'rails_helper'
gives us an access to all testing configurations and methods. :type => :helper
treats our tests as helper specs and provides us with specific methods.
That’s how the navigation_helper_spec.rb
file should look like when the collapsible_links_partial_path
method is tested.
That's how the navigation_helper_spec.rb
file should look like when the collapsible_links_partial_path
method is tested.
To learn more about the context
and it
, read the basic structure docs. Here we test two cases — when a user is logged in and when a user is not logged in. In each context of signed in user
and non-signed in user
, we have before hooks. Inside the corresponding context, those hooks (methods) run before each our tests. In our case, before each test we run the stub method, so the user_signed_in?
returns whatever value we tell it to return.
To learn more about the context
and it
, read the basic structure docs. Here we test two cases — when a user is logged in and when a user is not logged in. In each context of signed in user
and non-signed in user
, we have before hooks . Inside the corresponding context, those hooks (methods) run before each our tests. In our case, before each test we run the stub method, so the user_signed_in?
returns whatever value we tell it to return.
And finally, with the expect method we check that when we call collapsible_links_partial_path
method, we get an expected return value.
And finally, with the expect method we check that when we call collapsible_links_partial_path
method, we get an expected return value.
To run all tests, simply run:
To run all tests, simply run:
rspec spec
To run specifically the navigation_helper_spec.rb
file, run:
To run specifically the navigation_helper_spec.rb
file, run:
rspec spec/helpers/navigation_helper_spec.rb
If the tests passed, the output should look similar to this:
If the tests passed, the output should look similar to this:
Commit the changes.
Commit the changes.
git add -A
git commit -m "Add specs to NavigationHelper's collapsible_links_partial_path method"
Factories
Factories
Next, we’ll need some sample data to perform our tests. factory_girl
gem gives us ability to add sample data very easily, whenever we need it. Also it provides a good quality docs, so it makes the overall experience pretty pleasant. The only object we can create with our app so far is the User
. To define the user factory, create a factories
directory inside the spec
directory.
Next, we'll need some sample data to perform our tests. factory_girl
gem gives us ability to add sample data very easily, whenever we need it. Also it provides a good quality docs, so it makes the overall experience pretty pleasant. The only object we can create with our app so far is the User
. To define the user factory, create a factories
directory inside the spec
directory.
spec/factories
Inside the factories directory create a new file users.rb and add the following code:
Inside the factories directory create a new file users.rb and add the following code:
Now within our specs, we can easily create new users inside the test database, whenever we need them, using factory_girl
gem’s methods. For the comprehensive guide how to define and use factories, checkout the factory_girl
gem’s docs.
Now within our specs, we can easily create new users inside the test database, whenever we need them, using factory_girl
gem's methods. For the comprehensive guide how to define and use factories, checkout the factory_girl
gem's docs.
Our defined factory, user
, is pretty straightforward. We defined the values, user
objects will have. Also we used the sequence
method. By reading docs, you can see that with every additional User
record, n
value gets incremented by one. I.e. the first created user‘s name is going to be test0
, the second one’s test1
, etc.
Our defined factory, user
, is pretty straightforward. We defined the values, user
objects will have. Also we used the sequence
method. By reading docs, you can see that with every additional User
record, n
value gets incremented by one. Ie the first created user's name is going to be test0
, the second one's test1
, etc.
Commit the changes
Commit the changes
git add -A
git commit -m "add a users factory"
Feature specs
Feature specs
In the feature specs we write code which simulates a user’s interaction with an app. Feature specs are powered by the capybara
gem.
In the feature specs we write code which simulates a user's interaction with an app. Feature specs are powered by the capybara
gem.
Good news is that we’ve everything set up and ready to write our first feature specs. We’re going to test the login, logout and signup functionalities.
Good news is that we've everything set up and ready to write our first feature specs. We're going to test the login, logout and signup functionalities.
Inside the spec
directory, create a new directory called features
.
Inside the spec
directory, create a new directory called features
.
spec/features
Inside the features
directory, create another directory called user
.
Inside the features
directory, create another directory called user
.
spec/features/user
Inside the user
directory, create a new file called login_spec.rb
Inside the user
directory, create a new file called login_spec.rb
That’s how the login test looks like:
That's how the login test looks like:
With this code we simulate a visit to the login page, starting from the home page. Then we fill the form and submit it. Finally, we check if we have the #user-settings
element on the navigation bar, which is available only for signed in users.
With this code we simulate a visit to the login page, starting from the home page. Then we fill the form and submit it. Finally, we check if we have the #user-settings
element on the navigation bar, which is available only for signed in users.
feature
and scenario
are part of the Capybara’s syntax. feature
is the same as context
/describe
and scenario
is the same as it
. More info you can find in Capybara’s docs, Using Capybara With Rspec.
feature
and scenario
are part of the Capybara's syntax. feature
is the same as context
/ describe
and scenario
is the same as it
. More info you can find in Capybara's docs, Using Capybara With Rspec .
let
method allows us to write memorized methods which we could use across all specs within the context, the method was defined.
let
method allows us to write memorized methods which we could use across all specs within the context, the method was defined.
Here we also use our created users
factory and the create
method, which comes with the factory_girl
gem.
Here we also use our created users
factory and the create
method, which comes with the factory_girl
gem.
js: true
argument allows to test functionalities which involves JavaScript.
js: true
argument allows to test functionalities which involves JavaScript.
As always, to see if a test passes, run a specific file. In this case it is the login_spec.rb
file:
As always, to see if a test passes, run a specific file. In this case it is the login_spec.rb
file:
rspec spec/features/user/login_spec.rb
Commit the changes.
Commit the changes.
git add -A
git commit -m "add login feature specs"
Now we can test the logout functionality. Inside the user
directory, create a new file named logout_spec.rb
Now we can test the logout functionality. Inside the user
directory, create a new file named logout_spec.rb
spec/features/user/logout_spec.rb
The implemented test should look like this:
The implemented test should look like this:
The code simulates a user clicking the logout button and then expects to see non-logged in user’s links on the navigation bar.
The code simulates a user clicking the logout button and then expects to see non-logged in user's links on the navigation bar.
sign_in
method is one of the Devise helper methods. We have included those helper methods inside the rails_helper.rb
file previously.
sign_in
method is one of the Devise helper methods. We have included those helper methods inside the rails_helper.rb
file previously.
Run the file to see if the test passes.
Run the file to see if the test passes.
Commit the changes.
Commit the changes.
git add -A
git commit -m "add logout feature specs"
The last functionality we have is ability to sign up a new account. Let’s test it. Inside the user
directory create a new file named sign_up_spec.rb
. That’s how the file with the test inside should look like:
The last functionality we have is ability to sign up a new account. 让我们测试一下。 Inside the user
directory create a new file named sign_up_spec.rb
. That's how the file with the test inside should look like:
We simulate a user navigating to the signup page, filling the form, submitting the form and finally, we expect to see the #user-settings
element which is available only for logged in users.
We simulate a user navigating to the signup page, filling the form, submitting the form and finally, we expect to see the #user-settings
element which is available only for logged in users.
Here we use the Devise’s build
method instead of create
. This way we create a new object without saving it to the database.
Here we use the Devise's build
method instead of create
. This way we create a new object without saving it to the database.
We can run the whole test suite and see if all tests pass successfully.
We can run the whole test suite and see if all tests pass successfully.
rspec spec
Commit the changes.
Commit the changes.
git add -A
git commit -m "add sign up features specs"
We’re done with our first tests. So let’s merge the specs
branch with the master
.
We're done with our first tests. So let's merge the specs
branch with the master
.
git checkout master
git merge specs
Specs branch isn’t needed anymore. Delete it q__o.
Specs branch isn't needed anymore. Delete it q__o.
git branch -D specs
Main feed (Main feed)
On the the home page we’re going to create a posts feed. This feed is going to display all type of posts in a card format.
On the the home page we're going to create a posts feed. This feed is going to display all type of posts in a card format.
Start by creating a new branch:
Start by creating a new branch:
git checkout -b main_feed
Generate a new model called Post
.
Generate a new model called Post
.
rails g model post
Then we’ll need a Category
model to categorize the posts:
Then we'll need a Category
model to categorize the posts:
rails g model category
Now let’s create some associations between User
, Category
and Post
models.
Now let's create some associations between User
, Category
and Post
models.
Every post is going to belong to a category and its author (user). Open the models’ files and add the associations.
Every post is going to belong to a category and its author (user). Open the models' files and add the associations.
class Post < ApplicationRecord
belongs_to :user
belongs_to :category
end
class User < ApplicationRecord
...
has_many :posts, dependent: :destroy
end
class Category < ApplicationRecord
has_many :posts
end
The dependent: :destroy
argument says, when a user gets deleted, all posts what the user has created will be deleted too.
The dependent: :destroy
argument says, when a user gets deleted, all posts what the user has created will be deleted too.
Now we’ve to define data columns and associations inside the migrations files.
Now we've to define data columns and associations inside the migrations files.
Now run the migration files:
Now run the migration files:
rails db:migrate
Commit the changes:
Commit the changes:
git add -A
git commit -m "
- Generate Post and Category models.
- Create associations between User, Post and Category models.
- Create categories and posts database tables."
Specs
Specs
We can test the newly created models. Later we’ll need sample data for the tests. Since a post belongs to a category, we also need sample data for categories to set up the associations.
We can test the newly created models. Later we'll need sample data for the tests. Since a post belongs to a category, we also need sample data for categories to set up the associations.
Create a category
factory inside the factories
directory.
Create a category
factory inside the factories
directory.
Create a post
factory inside the factories
directory
Create a post
factory inside the factories
directory
spec/factories/posts.rb
As you see, it’s very easy to set up an association for factories. All we had to do to set up user
and category
associations for the post
factory, is to write factories’ names inside the post
factory.
As you see, it's very easy to set up an association for factories. All we had to do to set up user
and category
associations for the post
factory, is to write factories' names inside the post
factory.
Commit the changes.
Commit the changes.
git add -A
git commit -m "Add post and category factories"
For now we’ll only test the associations, because that’s the only thing we wrote yet inside the models.
For now we'll only test the associations, because that's the only thing we wrote yet inside the models.
Open the post_spec.rb
Open the post_spec.rb
spec/models/post_spec.rb
Add specs for the associations, so the file should look like this:
Add specs for the associations, so the file should look like this:
We use the described_class
method to get the current context’s class. Which is basically the same as writing Post
in this case. Then we use reflect_on_association
method to check that it returns a correct association.
We use the described_class
method to get the current context's class. Which is basically the same as writing Post
in this case. Then we use reflect_on_association
method to check that it returns a correct association.
Do the same for other models.
Do the same for other models.
Commit the changes.
Commit the changes.
git add -A
git commit -m "Add specs for User, Category, Post models' associations"
Home page layout
Home page layout
Currently the home page has nothing inside, only the dummy text “Home page”. It’s time to create its layout with bootstrap. Open the home page’s view file views/pages/index.html.erb
and replace the file’s content with the following code to create the page’s layout:
Currently the home page has nothing inside, only the dummy text “Home page”. It's time to create its layout with bootstrap. Open the home page's view file views/pages/index.html.erb
and replace the file's content with the following code to create the page's layout:
Now add some CSS to define elements’ style and responsive behavior.
Now add some CSS to define elements' style and responsive behavior.
Inside the stylesheets/partials
directory create a new file home_page.scss
Inside the stylesheets/partials
directory create a new file home_page.scss
assets/stylesheets/partials/home_page.scss
In the file add the following CSS:
In the file add the following CSS:
Inside the mobile.scss
file’s max-width: 767px
media query add:
Inside the mobile.scss
file's max-width: 767px
media query add:
Now the home page should look like this on bigger screens
Now the home page should look like this on bigger screens
and like this on the smaller screens
and like this on the smaller screens
Commit the changes.
Commit the changes.
git add -A
git commit -m "
- Add the bootstrap layout to the home page
- Add CSS to make home page layout's stylistic and responsive design changes"
Seeds
Seeds
To display posts on the home page, at first we need to have them inside the database. Creating data manually is boring and time consuming. To automate this process, we’ll use seeds. Open the seeds.rb
file.
To display posts on the home page, at first we need to have them inside the database. Creating data manually is boring and time consuming. To automate this process, we'll use seeds. Open the seeds.rb
file.
db/seeds.rb
Add the following code:
添加以下代码:
As you see, we create seed_users
, seed_categories
and seed_posts
methods to create User
, Category
and Post
records inside the development database. Also the faker gem is used to generate dummy text. Add faker
gem to your Gemfile
As you see, we create seed_users
, seed_categories
and seed_posts
methods to create User
, Category
and Post
records inside the development database. Also the faker gem is used to generate dummy text. Add faker
gem to your Gemfile
gem 'faker'
and
和
bundle install
To seed data, using the seeds.rb
file, run a command
To seed data, using the seeds.rb
file, run a command
rails db:seed
Commit the changes.
Commit the changes.
git add -A
git commit -m "
- Add faker gem
- Inside the seeds.rb file create methods to generate
User, Category and Post records inside the development database"
Rendering the posts
Rendering the posts
To render the posts, we’ll need a posts
directory inside the views.
To render the posts, we'll need a posts
directory inside the views.
Generate a new controller called Posts
, so it will automatically create a posts
directory inside the views too.
Generate a new controller called Posts
, so it will automatically create a posts
directory inside the views too.
rails g controller posts
Since in our app the PagesController
is responsible for the homepage, we’ll need to query data inside the pages_controller.rb
file’s index
action. Inside the index
action retrieve some records from the posts
table. Assign the retrieved records to an instance variable, so the retrieved objects are going to be available inside the home page’s views.
Since in our app the PagesController
is responsible for the homepage, we'll need to query data inside the pages_controller.rb
file's index
action. Inside the index
action retrieve some records from the posts
table. Assign the retrieved records to an instance variable, so the retrieved objects are going to be available inside the home page's views.
If you aren’t familiar with ruby variables, read this guide.
If you aren't familiar with ruby variables, read this guide .
If you aren’t familiar with retrieving records from the database in Rails, read the Active Record Query Interface guide.
If you aren't familiar with retrieving records from the database in Rails, read the Active Record Query Interface guide.
The index
action should look something like this right now:
The index
action should look something like this right now:
Navigate to the home page’s template
Navigate to the home page's template
views/pages/index.html.erb
and inside the .main-content
element add
and inside the .main-content
element add
<%= render @posts %>
This will render all posts, which were retrieved inside the index
action. Because post
objects belong to the Post
class, Rails automatically tries to render the _post.html.erb
partial template which is located
This will render all posts, which were retrieved inside the index
action. Because post
objects belong to the Post
class, Rails automatically tries to render the _post.html.erb
partial template which is located
views/posts/_post.html.erb
We haven’t created this partial file yet, so create it and add the following code inside:
We haven't created this partial file yet, so create it and add the following code inside:
I’ve used a bootstrap card component here to achieve the desired style. Then I just stored post’s content and its path inside the element. Also I added a link which will lead to the full post.
I've used a bootstrap card component here to achieve the desired style. Then I just stored post's content and its path inside the element. Also I added a link which will lead to the full post.
So far we didn’t define any routes for posts. We need them right now, so let’s declare them. Open the routes.rb
file and add the following code inside the routes
So far we didn't define any routes for posts. We need them right now, so let's declare them. Open the routes.rb
file and add the following code inside the routes
Here I’ve used a resources method to declare routes for index
, show
, new
, edit
, create
, update
and destroy
actions. Then I’ve declared some custom collection
routes to access pages with multiple Post
instances. These pages are going to be dedicated for separate branches, we’ll create them later.
Here I've used a resources method to declare routes for index
, show
, new
, edit
, create
, update
and destroy
actions. Then I've declared some custom collection
routes to access pages with multiple Post
instances. These pages are going to be dedicated for separate branches, we'll create them later.
Restart the server and go to http://localhost:3000. You should see rendered posts on the screen. The application should look similar to this:
Restart the server and go to http://localhost:3000 . You should see rendered posts on the screen. The application should look similar to this:
Commit the changes.
Commit the changes.
git add -A
git commit -m "Display posts on the home page
- Generate Posts controller and create an index action.
Inside the index action retrieve Post records
- Declare routes for posts
- Create a _post.html.erb partial inside posts directory
- Render posts inside the home page's main content"
To start styling posts, create a new scss file inside the partials
directory:
To start styling posts, create a new scss file inside the partials
directory:
assets/stylesheets/partials/posts.scss
and inside the file add the following CSS:
and inside the file add the following CSS:
The home page should look similar to this:
The home page should look similar to this:
Commit the change.
Commit the change.
git add -A
git commit -m "Create a posts.scss file and add CSS to it"
Styling with JavaScript
Styling with JavaScript
Currently the site’s design is pretty dull. To create contrast, we’re going to color the posts. But instead of just coloring it with CSS, let’s color them with different color patterns every time a user refreshes the website. To do that we’ll use JavaScript. It’s probably a silly idea, but it’s fun c(o_u)?
Currently the site's design is pretty dull. To create contrast, we're going to color the posts. But instead of just coloring it with CSS, let's color them with different color patterns every time a user refreshes the website. To do that we'll use JavaScript. It's probably a silly idea, but it's fun c(o_u)?
Navigate to the javascripts
directory inside your assets
and create a new directory called posts
. Inside the directory create a new file called style.js
. Also if you want, you can delete by default generated .coffee
, files inside the javascripts
directory. We won’t use CoffeeScript in this tutorial.
Navigate to the javascripts
directory inside your assets
and create a new directory called posts
. Inside the directory create a new file called style.js
. Also if you want, you can delete by default generated .coffee
, files inside the javascripts
directory. We won't use CoffeeScript in this tutorial.
assets/javascripts/posts/style.js
Inside the style.js
file add the following code.
Inside the style.js
file add the following code.
With this piece of code we randomly set one of two style modes when a browser gets refreshed, by adding attributes to posts. One style has colored borders only, another style has solid color posts. With every page change and browser refresh we also recolor posts randomly too. Inside the randomColorSet()
function you can see predefined color schemes.
With this piece of code we randomly set one of two style modes when a browser gets refreshed, by adding attributes to posts. One style has colored borders only, another style has solid color posts. With every page change and browser refresh we also recolor posts randomly too. Inside the randomColorSet()
function you can see predefined color schemes.
mouseenter
and mouseleave
event handlers are going to be needed in the future for posts in specific pages. There posts’ style is going to be different than posts’ on the home page. When you’ll hover on a post, it will slightly change its bottom border’s color. You’ll see this later.
mouseenter
and mouseleave
event handlers are going to be needed in the future for posts in specific pages. There posts' style is going to be different than posts' on the home page. When you'll hover on a post, it will slightly change its bottom border's color. You'll see this later.
Commit the changes.
Commit the changes.
git add -A
git commit -m "Create a style.js file and add js to create posts' style"
To complement the styling, add some CSS. Open the posts.scss
file
To complement the styling, add some CSS. Open the posts.scss
file
assets/stylesheets/partials/posts.scss
and add the following CSS:
and add the following CSS:
Also inside the mobile.scss
add the following code to fix too large text issues on smaller screens:
Also inside the mobile.scss
add the following code to fix too large text issues on smaller screens:
The home page should look similar to this right now:
The home page should look similar to this right now:
Commit the changes
Commit the changes
git add -A
git commit -m "Add CSS to posts on the home page
- add CSS to the posts.scss file
- add CSS to the mobile.scss to fix too large text issues on smaller screens"
Modal window
Modal window
I want to be able to click on a post and see its full content, without going to another page. To achieve this functionality I’ll use a bootstrap’s modal component.
I want to be able to click on a post and see its full content, without going to another page. To achieve this functionality I'll use a bootstrap's modal component .
Inside the posts
directory, create a new partial file _modal.html.erb
Inside the posts
directory, create a new partial file _modal.html.erb
views/posts/_modal.html.erb
and add the following code:
and add the following code:
This is just a slightly modified bootstrap’s component to accomplish this particular task.
This is just a slightly modified bootstrap's component to accomplish this particular task.
Render this partial at the top of the home page’s template.
Render this partial at the top of the home page's template.
To make this modal window functional, we have to add some JavaScript. Inside the posts
directory, create a new file modal.js
To make this modal window functional, we have to add some JavaScript. Inside the posts
directory, create a new file modal.js
assets/javascripts/posts/modal.js
Inside the file, add the following code:
Inside the file, add the following code:
With this js code we simply store selected post’s data into variables and fill modal window’s elements with this data. Finally, with the last line of code we make the modal window visible.
With this js code we simply store selected post's data into variables and fill modal window's elements with this data. Finally, with the last line of code we make the modal window visible.
To enhance the modal window’s looks, add some CSS. But before adding CSS, let’s do a quick management task inside the stylesheets
directory.
To enhance the modal window's looks, add some CSS. But before adding CSS, let's do a quick management task inside the stylesheets
directory.
Inside the partials
directory create a new directory called posts
Inside the partials
directory create a new directory called posts
assets/stylesheets/partials/posts
Inside the posts
directory create a new file home_page.scss
. Cut all code from the posts.scss
file and paste it inside the home_page.scss
file. Delete the posts.scss
file. We’re doing this for a better CSS code management. It’s clearer when have few smaller CSS files with a distinguishable purpose, rather than one big file where everything is mashed together.
Inside the posts
directory create a new file home_page.scss
. Cut all code from the posts.scss
file and paste it inside the home_page.scss
file. Delete the posts.scss
file. We're doing this for a better CSS code management. It's clearer when have few smaller CSS files with a distinguishable purpose, rather than one big file where everything is mashed together.
Also inside the posts
directory, create a new file modal.scss
and add the following CSS:
Also inside the posts
directory, create a new file modal.scss
and add the following CSS:
Now when we click on the post, the application should look like this:
Now when we click on the post, the application should look like this:
Commit the changes.
Commit the changes.
git add -A
git commit -m "Add a popup window to show a full post's content
- Add bootstrap's modal component to show full post's content
- Render the modal inside the home page's template
- Add js to fill the modal with post's content and show it
- Add CSS to style the modal"
Also merge the main_feed
branch with the master
Also merge the main_feed
branch with the master
git checkout master
git merge main_feed
Get rid of the main_feed
branch
Get rid of the main_feed
branch
git branch -D main_feed
Single post
Single post
Switch to a new branch
Switch to a new branch
git checkout -b single_post
Show a single post
Show a single post
If you try to click on the I'm interested
button, you will get an error. We haven’t created a show.html.erb
template nor we’ve created a corresponding controller’s action. By clicking the button I want to be redirected to a selected post’s page.
If you try to click on the I'm interested
button, you will get an error. We haven't created a show.html.erb
template nor we've created a corresponding controller's action. By clicking the button I want to be redirected to a selected post's page.
Inside the PostsController
, create a show
action and then query and store a specific post object inside an instance variable:
Inside the PostsController
, create a show
action and then query and store a specific post object inside an instance variable:
I'm interested
button redirects to a selected post. It has a href
attribute with a path to a post. By sending a GET
request to get a post, rails calls the show
action. Inside the show
action, we’ve an access to the id param, because by sending a GET
request to get a specific post, we provided its id
. I.e. by going to a /posts/1
path, we would send a request to get a post whose id
is 1
.
I'm interested
button redirects to a selected post. It has a href
attribute with a path to a post. By sending a GET
request to get a post, rails calls the show
action. Inside the show
action, we've an access to the id param, because by sending a GET
request to get a specific post, we provided its id
. Ie by going to a /posts/1
path, we would send a request to get a post whose id
is 1
.
Create a show.html.erb
template inside the posts
directory
Create a show.html.erb
template inside the posts
directory
views/posts/show.html.erb
Inside the file add the following code:
Inside the file add the following code:
Create a show.scss
file inside the posts
directory and add CSS to style the page’s look:
Create a show.scss
file inside the posts
directory and add CSS to style the page's look:
Here I defined the page’s height to be 100vh-50px
, so the page’s content is full viewport’s height. It allows the container to be colored white across the full browser’s height, no matter if there is enough of content inside the element or not. vh
property means viewport’s height, so 100vh
value means that the element is stretched 100% of viewport’s height. 100vh-50px
is required to subtract navigation bar’s height, otherwise the container would be stretched too much by 50px
.
Here I defined the page's height to be 100vh-50px
, so the page's content is full viewport's height. It allows the container to be colored white across the full browser's height, no matter if there is enough of content inside the element or not. vh
property means viewport's height, so 100vh
value means that the element is stretched 100% of viewport's height. 100vh-50px
is required to subtract navigation bar's height, otherwise the container would be stretched too much by 50px
.
If you click on the I'm interested
button now, you will be redirected to a page which looks similar to this:
If you click on the I'm interested
button now, you will be redirected to a page which looks similar to this:
We’ll add extra features to the show.html.erb
template later. Now commit the changes.
We'll add extra features to the show.html.erb
template later. Now commit the changes.
git add -A
git commit -m "Create a show template for posts
- Add a show action and query a post to an instance variable
- Create a show.scss file and add CSS"
Specs
Specs
Instead of manually checking that this functionality, of modal window appearance and redirection to a selected post, works, wrap it all with specs. We’re going to use capybara to simulate a user’s interaction with the app.
Instead of manually checking that this functionality, of modal window appearance and redirection to a selected post, works, wrap it all with specs. We're going to use capybara to simulate a user's interaction with the app.
Inside the features
directory, create a new directory called posts
Inside the features
directory, create a new directory called posts
spec/features/posts
Inside the new directory, create a new file visit_single_post_spec.rb
Inside the new directory, create a new file visit_single_post_spec.rb
spec/features/posts/visit_single_post_spec.rb
And add a feature spec inside. The file looks like this:
And add a feature spec inside. The file looks like this:
Here I defined all steps which I would perform manually. I start by going to the home page, click on the post, expect to see the popped up modal window, click on the I'm interested
button, and finally, expect to be redirected to the post’s page and see its content.
Here I defined all steps which I would perform manually. I start by going to the home page, click on the post, expect to see the popped up modal window, click on the I'm interested
button, and finally, expect to be redirected to the post's page and see its content.
By default RSpec matchers have_selector
, have_css
, etc. return true if an element is actually visible to a user. So after it was clicked on a post, testing framework expects to see a visible modal window. If you don’t care if a user sees an element or not and you just care about an element’s presence in the DOM, pass an additional visible: false
argument.
By default RSpec matchers have_selector
, have_css
, etc. return true if an element is actually visible to a user. So after it was clicked on a post, testing framework expects to see a visible modal window. If you don't care if a user sees an element or not and you just care about an element's presence in the DOM, pass an additional visible: false
argument.
Try to run the test
Try to run the test
rspec spec/features/posts/visit_single_post_spec.rb
Commit the changes.
Commit the changes.
git add -A
git commit -m "Add a feature spec to test if a user can go to a
single post from the home page"
Merge the single_post
branch with the master
.
Merge the single_post
branch with the master
.
git checkout master
git merge single_post
git branch -D single_post
Specific branches
Specific branches
Every post belongs to a particular branch. Let’s create specific pages for different branches.
Every post belongs to a particular branch. Let's create specific pages for different branches.
Switch to a new branch
Switch to a new branch
git checkout -b specific_branches
Home page’s side menu
Home page's side menu
Start by updating the home page’s side menu. Add links to specific branches. Open the index.html.erb
file:
Start by updating the home page's side menu. Add links to specific branches. Open the index.html.erb
file:
views/pages/index.html.erb
We are going to put some links inside the #side-menu
element. Split file’s content into partials, otherwise it will get noisy very quickly. Cut #side-menu
and #main-content
elements, and paste them into separate partial files. Inside the pages
directory create an index
directory, and inside the directory create corresponding partial files to the elements. The files should look like this:
We are going to put some links inside the #side-menu
element. Split file's content into partials, otherwise it will get noisy very quickly. Cut #side-menu
and #main-content
elements, and paste them into separate partial files. Inside the pages
directory create an index
directory, and inside the directory create corresponding partial files to the elements. The files should look like this:
Render those partial files inside the home page’s template. The file should look like this:
Render those partial files inside the home page's template. 该文件应如下所示:
Commit the changes.
Commit the changes.
git add -A
git commit -m "Split home page template's content into partials"
Inside the _side_menu.html.erb
partial add a list of links, so the file should look like this:
Inside the _side_menu.html.erb
partial add a list of links, so the file should look like this:
An unordered list was added. Inside the list we render another partial with links. Those links are going to be available for all users, no matter if they are signed in or not. Create this partial file and add the links.
An unordered list was added. Inside the list we render another partial with links. Those links are going to be available for all users, no matter if they are signed in or not. Create this partial file and add the links.
Inside the index
directory create a side_menu
directory:
Inside the index
directory create a side_menu
directory:
views/pages/index/side_menu
Inside the directory create a _no_login_required_links.html.erb
partial with the following code:
Inside the directory create a _no_login_required_links.html.erb
partial with the following code:
Here we simply added links to specific branches of posts. If you are wondering how do we have paths, such as hobby_posts_path
, etc., look at the routes.rb
file. Previously we’ve added nested collection
routes inside the resources :posts
declaration.
Here we simply added links to specific branches of posts. If you are wondering how do we have paths, such as hobby_posts_path
, etc., look at the routes.rb
file. Previously we've added nested collection
routes inside the resources :posts
declaration.
If you pay attention to i
elements’ attributes, you will notice fa
classes. With those classes we declare Font Awesome icons. We haven’t set up this library yet. Fortunately, it’s very easy to set up. Inside the main application.html.erb
file’s head
element, add the following line
If you pay attention to i
elements' attributes, you will notice fa
classes. With those classes we declare Font Awesome icons. We haven't set up this library yet. Fortunately, it's very easy to set up. Inside the main application.html.erb
file's head
element, add the following line
The side menu should be present now.
The side menu should be present now.
Commit the changes.
Commit the changes.
git add -A
git commit -m "Add links to the home page's side menu"
On smaller screens, where width is between 767px
and 1000px
, bootstrap’s container looks unpleasant, it looks overly compressed. So stretch it among those widths. Inside the mobile.scss
file, add the following code:
On smaller screens, where width is between 767px
and 1000px
, bootstrap's container looks unpleasant, it looks overly compressed. So stretch it among those widths. Inside the mobile.scss
file, add the following code:
Commit the change.
Commit the change.
git add -A
git commit -m "set .container width to 100%
when viewport's width is between 767px and 1000px"
Branch page
Branch page
If you try to click on one of those side menu links, you will get an error. We haven’t set up actions inside the PostsController
nor we created any templates for it.
If you try to click on one of those side menu links, you will get an error. We haven't set up actions inside the PostsController
nor we created any templates for it.
Inside the PostsController
, define hobby
, study
, and team
actions.
Inside the PostsController
, define hobby
, study
, and team
actions.
Inside every action, posts_for_branch
method is called. This method will return data for the specific page, depending on the action’s name. Define the method inside the private
scope.
Inside every action, posts_for_branch
method is called. This method will return data for the specific page, depending on the action's name. Define the method inside the private
scope.
In the @categories
instance variable we retrieve all categories for a specific branch. I.e. if you go to the hobby branch page, all categories which belong to the hobby branch will be retrieved.
In the @categories
instance variable we retrieve all categories for a specific branch. Ie if you go to the hobby branch page, all categories which belong to the hobby branch will be retrieved.
To get and store posts inside the @posts
instance variable, get_posts
method is used and then it is chained with a paginate
method. paginate
method comes from will_paginate gem. Let’s start by defining the get_posts
method. Inside the PostsController
’s private
scope add:
To get and store posts inside the @posts
instance variable, get_posts
method is used and then it is chained with a paginate
method. paginate
method comes from will_paginate gem. Let's start by defining the get_posts
method. Inside the PostsController
's private
scope add:
Right now get_posts
method just retrieves any 30 posts, not specific to anything, so we could move on and focus on further development. We’ll come back to this method in the near future.
Right now get_posts
method just retrieves any 30 posts, not specific to anything, so we could move on and focus on further development. We'll come back to this method in the near future.
Add the will_paginate
gem to be able to use pagination.
Add the will_paginate
gem to be able to use pagination.
gem 'will_paginate', '~> 3.1.0'
run
跑
bundle install
All we miss now is templates. They are going to be similar to all branches, so instead of repeating the code, inside every of those branches, create a partial with a general structure for a branch. Inside the posts
directory create a _branch.html.erb
file.
All we miss now is templates. They are going to be similar to all branches, so instead of repeating the code, inside every of those branches, create a partial with a general structure for a branch. Inside the posts
directory create a _branch.html.erb
file.
First you see a page_title
variable being printed on the page. We’ll pass this variable as an argument when we’ll render the _branch.html.erb
partial. Next, a _create_new_post
partial is rendered to display a link, which will lead to a page, where a user could create a new post. Create this partial file inside a new branch
directory:
First you see a page_title
variable being printed on the page. We'll pass this variable as an argument when we'll render the _branch.html.erb
partial. Next, a _create_new_post
partial is rendered to display a link, which will lead to a page, where a user could create a new post. Create this partial file inside a new branch
directory:
Here we’ll use a create_new_post_partial_path
helper method to determine which partial file to render. Inside the posts_helper.rb
file, implement the method:
Here we'll use a create_new_post_partial_path
helper method to determine which partial file to render. Inside the posts_helper.rb
file, implement the method:
Also create those two corresponding partials inside a new create_new_post
directory:
Also create those two corresponding partials inside a new create_new_post
directory:
Next, inside the _branch.html.erb
file we render a list of categories. Create a _categories.html.erb
partial file:
Next, inside the _branch.html.erb
file we render a list of categories. Create a _categories.html.erb
partial file:
Inside the file, we have a all_categories_button_partial_path
helper method which determines which partial file to render. Define this method inside the posts_helper.rb
file:
Inside the file, we have a all_categories_button_partial_path
helper method which determines which partial file to render. Define this method inside the posts_helper.rb
file:
All categories are going to be selected by default. If the params[:category]
is empty, it means that none categories were selected by a user, which means that currently the default value all
is selected. Create the corresponding partial files:
All categories are going to be selected by default. If the params[:category]
is empty, it means that none categories were selected by a user, which means that currently the default value all
is selected. Create the corresponding partial files:
The send method is used here to call a method by using a string, this allows to be flexible and call methods dynamically. In our case we generate different paths, depending on the current controller’s action.
The send method is used here to call a method by using a string, this allows to be flexible and call methods dynamically. In our case we generate different paths, depending on the current controller's action.
Next, inside the _branch.html.erb
file we render posts and call the no_posts_partial_path
helper method. If posts are not found, the method will display a message.
Next, inside the _branch.html.erb
file we render posts and call the no_posts_partial_path
helper method. If posts are not found, the method will display a message.
Inside the posts_helper.rb
add the helper method:
Inside the posts_helper.rb
add the helper method:
Here I use a ternary operator, so the code looks a little bit cleaner. If there are any posts, I don’t want to show any messages. Since you cannot pass an empty string to the render
method, I pass a path to an empty partial instead, in occasions where I don’t want to render anything.
Here I use a ternary operator, so the code looks a little bit cleaner. If there are any posts, I don't want to show any messages. Since you cannot pass an empty string to the render
method, I pass a path to an empty partial instead, in occasions where I don't want to render anything.
Create a shared
directory inside the views and then create an empty partial:
Create a shared
directory inside the views and then create an empty partial:
views/shared/_empty_partial.html.erb
Now create a _no_posts.html.erb
partial for the message inside the branch
directory.
Now create a _no_posts.html.erb
partial for the message inside the branch
directory.
Finally, we use the will_paginate
method from the gem to split posts into multiple pages if there are a lot of posts.
Finally, we use the will_paginate
method from the gem to split posts into multiple pages if there are a lot of posts.
Create templates for hobby
, study
and team
actions. Inside them we’ll render the _branch.html.erb
partial file and pass specific local variables.
Create templates for hobby
, study
and team
actions. Inside them we'll render the _branch.html.erb
partial file and pass specific local variables.
If you go to any of those branch pages, you will see something like this
If you go to any of those branch pages, you will see something like this
Also if you scroll down, you will see that now we have a pagination
Also if you scroll down, you will see that now we have a pagination
We’ve done quite a lot of work to create these branch pages. Commit the changes
We've done quite a lot of work to create these branch pages. Commit the changes
git add -A
git commit -m "Create branch pages for specific posts
- Inside the PostsController define hobby, study and team actions.
Define a posts_for_branch method and call it inside these actions
- Add will_paginate gem
- Create a _branch.html.erb partial file
- Create a _create_new_post.html.erb partial file
- Define a create_new_post_partial_path helper method
- Create a _signed_in.html.erb partial file
- Create a _not_signed_in.html.erb partial file
- Create a _categories.html.erb partial file
- Define a all_categories_button_partial_path helper method
- Create a _all_selected.html.erb partial file
- Create a _all_not_selected.html.erb partial file
- Define a no_posts_partial_path helper method
- Create a _no_posts.html.erb partial file
- Create a hobby.html.erb template file
- Create a study.html.erb template file
- Create a team.html.erb template file"
Specs
Specs
Cover helper methods with specs. The posts_helper_spec.rb
file should look like this:
Cover helper methods with specs. The posts_helper_spec.rb
file should look like this:
Again, specs are pretty simple here. I used the stub
method to define methods’ return values. To define params, I selected the controller and simply defined it like this controller.params[:param_name]
. And finally, I assigned instance variables by using an assign method.
Again, specs are pretty simple here. I used the stub
method to define methods' return values. To define params, I selected the controller and simply defined it like this controller.params[:param_name]
. And finally, I assigned instance variables by using an assign method.
Commit the changes
Commit the changes
git add -A
git commit -m "Add specs for PostsHelper methods"
Design changes
Design changes
In these branch pages we want to have different posts’ design. In the home page we have the cards design. In branch pages let’s create a list design, so a user could see more posts and browse through them more efficiently.
In these branch pages we want to have different posts' design. In the home page we have the cards design. In branch pages let's create a list design, so a user could see more posts and browse through them more efficiently.
Inside the posts
directory, create a post
directory with a _home_page.html.erb
partial inside.
Inside the posts
directory, create a post
directory with a _home_page.html.erb
partial inside.
posts/post/_home_page.html.erb
Cut the _post.html.erb
partial’s content and paste it inside the _home_page.html.erb
partial file. Inside the _post.html.erb
partial file add the following line of code:
Cut the _post.html.erb
partial's content and paste it inside the _home_page.html.erb
partial file. Inside the _post.html.erb
partial file add the following line of code:
Here we call the post_format_partial_path
helper method to decide which post design to render, depending on the current path. If a user is on the home page, render the post design for the home page. If a user is on the branch page, render the post design for the branch page. That’s why we cut _post.html.erb
file’s content into _home_page.html.erb
file.
Here we call the post_format_partial_path
helper method to decide which post design to render, depending on the current path. If a user is on the home page, render the post design for the home page. If a user is on the branch page, render the post design for the branch page. That's why we cut _post.html.erb
file's content into _home_page.html.erb
file.
Inside the post
directory, create a new _branch_page.html.erb
file and paste this code to define the posts design for the branch page.
Inside the post
directory, create a new _branch_page.html.erb
file and paste this code to define the posts design for the branch page.
To decide which partial file to render, define the post_format_partial_path
helper method inside the posts_helper.rb
To decide which partial file to render, define the post_format_partial_path
helper method inside the posts_helper.rb
The post_format_partial_path
helper method won’t be available in the home page, because we render posts inside the home page’s template, which belongs to a different controller. To have an access to this method, inside the home page’s template, include PostsHelper
inside the ApplicationHelper
The post_format_partial_path
helper method won't be available in the home page, because we render posts inside the home page's template, which belongs to a different controller. To have an access to this method, inside the home page's template, include PostsHelper
inside the ApplicationHelper
include PostsHelper
Specs
Specs
Add specs for the post_format_partial_path
helper method:
Add specs for the post_format_partial_path
helper method:
Commit the changes
Commit the changes
git add -A
git commit -m "Add specs for the post_format_partial_path helper method"
CSS
CSS
Describe the posts style in branch pages with CSS. Inside the posts
directory, create a new branch_page.scss
style sheet file:
Describe the posts style in branch pages with CSS. Inside the posts
directory, create a new branch_page.scss
style sheet file:
Inside the base/default.scss
add:
Inside the base/default.scss
add:
To fix style issues on smaller devices, inside the responsive/mobile.scss
add:
To fix style issues on smaller devices, inside the responsive/mobile.scss
add:
Now the branch pages should look like this:
Now the branch pages should look like this:
Commit the changes.
Commit the changes.
git add -A
git commit -m "Describe the posts style in branch pages
- Create a branch_page.scss file and add CSS
- Add CSS to the default.scss file
- Add CSS to the mobile.scss file"
Search bar (Search bar)
We want not only be able to browse through posts, but also search for specific ones. Inside the _branch.html.erb
partial file, above the categories
row, add:
We want not only be able to browse through posts, but also search for specific ones. Inside the _branch.html.erb
partial file, above the categories
row, add:
Create a _search_form.html.erb
partial file inside the branch
directory and add the following code inside:
Create a _search_form.html.erb
partial file inside the branch
directory and add the following code inside:
Here with the send
method we dynamically generate a path to a specific PostsController
’s action, depending on a current branch. Also we send an extra data field for the category if a specific category is selected. If a user has selected a specific category, only search results from that category will be returned.
Here with the send
method we dynamically generate a path to a specific PostsController
's action, depending on a current branch. Also we send an extra data field for the category if a specific category is selected. If a user has selected a specific category, only search results from that category will be returned.
Define the category_field_partial_path
helper method inside the posts_helper.rb
Define the category_field_partial_path
helper method inside the posts_helper.rb
Create a _category_field.html.erb
partial file and add the code:
Create a _category_field.html.erb
partial file and add the code:
To give the search form some style, add CSS to the branch_page.scss
file:
To give the search form some style, add CSS to the branch_page.scss
file:
The search form, in branch pages, should look likes this now
The search form, in branch pages, should look likes this now
Commit the changes
Commit the changes
git add -A
git commit -m "Add a search form in branch pages
- Render a search form inside the _branch.html.erb
- Create a _search_form.html.erb partial file
- Define a category_field_partial_path helper method in PostsHelper
- Create a _category_field.html.erb partial file
- Add CSS for the the search form in branch_page.scss"
Currently our form isn’t really functional. We could use some gems to achieve search functionality, but our data isn’t complicated, so we can create our own simple search engine. We’ll use scopes inside the Post
model to make queries chainable and some conditional logic inside the controller (we will extract it into service object in the next section to make the code cleaner).
Currently our form isn't really functional. We could use some gems to achieve search functionality, but our data isn't complicated, so we can create our own simple search engine. We'll use scopes inside the Post
model to make queries chainable and some conditional logic inside the controller (we will extract it into service object in the next section to make the code cleaner).
Start by defining scopes inside the Post
model. To warm up, define the default_scope
inside the post.rb
file. This orders posts in descending order by the creation date, newest posts are at the top.
Start by defining scopes inside the Post
model. To warm up, define the default_scope
inside the post.rb
file. This orders posts in descending order by the creation date, newest posts are at the top.
Commit the change
Commit the change
git add -A
git commit -m "Define a default_scope for posts"
Make sure that the default_scope
works correctly by wrapping it with a spec. Inside the post_spec.rb
file, add:
Make sure that the default_scope
works correctly by wrapping it with a spec. Inside the post_spec.rb
file, add:
Commit the change:
Commit the change:
git add -A
git commit -m "Add a spec for the Post model's default_scope"
Now let’s make the search bar functional. Inside the posts_controller.rb
replace the get_posts
method’s content with:
Now let's make the search bar functional. Inside the posts_controller.rb
replace the get_posts
method's content with:
As I’ve mentioned a little bit earlier, logic, just like in views, isn’t really a good place in controllers. We want to make them clean. So we’ll extract the logic out of this method in the upcoming section.
As I've mentioned a little bit earlier, logic, just like in views, isn't really a good place in controllers. We want to make them clean. So we'll extract the logic out of this method in the upcoming section.
As you see, there is some conditional logic going on. Depending on a user request, data gets queried differently using scopes.
As you see, there is some conditional logic going on. Depending on a user request, data gets queried differently using scopes.
Inside the Post
model, define those scopes:
Inside the Post
model, define those scopes:
The joins
method is used to query records from the associated tables. Also the basic SQL syntax is used to find records, based on provided strings.
The joins
method is used to query records from the associated tables. Also the basic SQL syntax is used to find records, based on provided strings.
Now if you restart the server and go back to any of those branch pages, the search bar should work! Also now you can filter posts by clicking on category buttons. And also when you select a particular category, only posts from that category are queried when you use the search form.
Now if you restart the server and go back to any of those branch pages, the search bar should work! Also now you can filter posts by clicking on category buttons. And also when you select a particular category, only posts from that category are queried when you use the search form.
Commit the changes
Commit the changes
git add -A
git commit -m "Make search bar and category filters
in branch pages functional
- Add by_category, by_branch and search scopes in the Post model
- Modify the get_posts method in PostsController"
Cover these scopes with specs. Inside the post_spec.rb
file’s Scopes
context add:
Cover these scopes with specs. Inside the post_spec.rb
file's Scopes
context add:
Commit the changes
Commit the changes
git add -A
git commit -m "Add specs for Post model's
by_branch, by_category and search scopes"
Infinite scroll (Infinite scroll)
When you go to any of these branch pages, at the bottom of the page you see the pagination
When you go to any of these branch pages, at the bottom of the page you see the pagination
When you click on the next link, it redirects you to another page with older posts. Instead of redirecting to another page with older posts, we can make an infinite scrolling functionality, similar to the Facebook’s and Twitter’s feed. You just scroll down and without any redirection and page reload, older posts are appended to the bottom of the list. Surprisingly, it is very easy to achieve. All we have to do is write some JavaScript. Whenever a user reaches the bottom of the page, AJAX request is sent to get data from the next
page and that data gets appended to the bottom of the list.
When you click on the next link, it redirects you to another page with older posts. Instead of redirecting to another page with older posts, we can make an infinite scrolling functionality, similar to the Facebook's and Twitter's feed. You just scroll down and without any redirection and page reload, older posts are appended to the bottom of the list. Surprisingly, it is very easy to achieve. All we have to do is write some JavaScript. Whenever a user reaches the bottom of the page, AJAX request is sent to get data from the next
page and that data gets appended to the bottom of the list.
Start by configuring the AJAX request and its conditions. When a user passes a certain threshold by scrolling down, AJAX request gets fired. Inside the javascripts/posts
directory, create a new infinite_scroll.js
file and add the code:
Start by configuring the AJAX request and its conditions. When a user passes a certain threshold by scrolling down, AJAX request gets fired. Inside the javascripts/posts
directory, create a new infinite_scroll.js
file and add the code:
The isLoading
variable makes sure that only one request is sent at a time. If there is currently a request in progress, other requests won’t be initiated.
The isLoading
variable makes sure that only one request is sent at a time. If there is currently a request in progress, other requests won't be initiated.
First check if pagination is present, if there are any more posts to render. Next, get a link to the next page, this is where the data will be retrieved from. Then set a threshold when to call an AJAX request, in this case the threshold is 60px
from the bottom of the window. Finally, if all conditions successfully pass, load data from the next
page using the getScript()
function.
First check if pagination is present, if there are any more posts to render. Next, get a link to the next page, this is where the data will be retrieved from. Then set a threshold when to call an AJAX request, in this case the threshold is 60px
from the bottom of the window. Finally, if all conditions successfully pass, load data from the next
page using the getScript()
function.
Because the getScript()
function loads the JavaScript file, we have to specify which file to render inside the PostsController
. Inside the posts_for_branch
method specify respond_to
formats and which files to render.
Because the getScript()
function loads the JavaScript file, we have to specify which file to render inside the PostsController
. Inside the posts_for_branch
method specify respond_to
formats and which files to render.
When the controller tries to respond with the .js
file, theposts_pagination_page
template gets rendered. This partial file appends newly retrieved posts to the list. Create this file to append new posts and update the pagination element.
When the controller tries to respond with the .js
file, the posts_pagination_page
template gets rendered. This partial file appends newly retrieved posts to the list. Create this file to append new posts and update the pagination element.
Create an update_pagination_partial_path
helper method inside the posts_helper.rb
Create an update_pagination_partial_path
helper method inside the posts_helper.rb
Here the next_page
method from the will_paginate
gem is used, to determine if there are any more posts to load in the future or not.
Here the next_page
method from the will_paginate
gem is used, to determine if there are any more posts to load in the future or not.
Create the corresponding partial files:
Create the corresponding partial files:
If you go to any of the branch pages and scroll down, older posts should be automatically appended to the list.
If you go to any of the branch pages and scroll down, older posts should be automatically appended to the list.
Also we no longer need to see the pagination menu, so hide it with CSS. Inside the branch_page.scss
file add:
Also we no longer need to see the pagination menu, so hide it with CSS. Inside the branch_page.scss
file add:
Commit the changes
Commit the changes
git add -A
git commit -m "Transform posts pagination into infinite scroll
- Create an infinite_scroll.js file
- Inside PostController's posts_for_branch method add respond_to format
- Define an update_pagination_partial_path
- Create _update_pagination.js.erb and _remove_pagination.js.erb partials
- hide the .infinite-scroll element with CSS"
Specs
Specs
Cover the update_pagination_partial_path
helper method with specs:
Cover the update_pagination_partial_path
helper method with specs:
Here I’ve used a test double
to simulate the posts
instance variable and its chained method next_page
. You can learn more about the RSpec Mocks here.
Here I've used a test double
to simulate the posts
instance variable and its chained method next_page
. You can learn more about the RSpec Mocks here .
Commit the changes:
Commit the changes:
git add -A
git commit -m "Add specs for the update_pagination_partial_path
helper method"
We can also write feature specs to make sure that posts are successfully appended, after you scroll down. Create an infinite_scroll_spec.rb
file:
We can also write feature specs to make sure that posts are successfully appended, after you scroll down. Create an infinite_scroll_spec.rb
file:
In the spec file all branch pages are covered. We make sure that this functionality works on all three pages. The per_page
is will_paginate
gem’s method. Here the Post
model is selected and the default number of posts per page is set.
In the spec file all branch pages are covered. We make sure that this functionality works on all three pages. The per_page
is will_paginate
gem's method. Here the Post
model is selected and the default number of posts per page is set.
The check_posts_count
method is defined to reduce the amount of code the file has. Instead of repeating the same code over and over again in different specs, we extracted it into a single method. Once the page is visited, it is expected to see 15 posts. Then the execute_script
method is used to run JavaScript, which scrolls the scrollbar to the browser’s bottom. Finally, after the scroll, it is expected to see an additional 15 posts. Now in total there should be 30 posts on the page.
The check_posts_count
method is defined to reduce the amount of code the file has. Instead of repeating the same code over and over again in different specs, we extracted it into a single method. Once the page is visited, it is expected to see 15 posts. Then the execute_script
method is used to run JavaScript, which scrolls the scrollbar to the browser's bottom. Finally, after the scroll, it is expected to see an additional 15 posts. Now in total there should be 30 posts on the page.
Commit the changes:
Commit the changes:
git add -A
git commit -m "Add feature specs for posts' infinite scroll functionality"
Home page update
Home page update
Currently on the home page we can only see few random posts. Modify the home page, so we could see a few posts from all branches.
Currently on the home page we can only see few random posts. Modify the home page, so we could see a few posts from all branches.
Replace the _main_content.html.erb
file’s content with:
Replace the _main_content.html.erb
file's content with:
We created sections with posts for every branch.
We created sections with posts for every branch.
Define instance variables inside the PagesController
’s index
action. The action should look like this:
Define instance variables inside the PagesController
's index
action. The action should look like this:
We have the no_posts_partial_path
helper method from before, but we should modify it a little bit and make it more reusable. Currently it works only for branch pages. Add a posts
parameter to the method, so it should look like this now:
We have the no_posts_partial_path
helper method from before, but we should modify it a little bit and make it more reusable. Currently it works only for branch pages. Add a posts
parameter to the method, so it should look like this now:
Here the posts
parameter was added, instance variable was changed to a simple variable and the partial’s path was changed too. So move the _no_posts.html.erb
partial file from
Here the posts
parameter was added, instance variable was changed to a simple variable and the partial's path was changed too. So move the _no_posts.html.erb
partial file from
posts/branch/_no_posts.html.erb
to
至
posts/shared/_no_posts.html.erb
Also inside the _branch.html.erb
file pass the @posts
instance variable to the no_posts_partial_path
method as an argument.
Also inside the _branch.html.erb
file pass the @posts
instance variable to the no_posts_partial_path
method as an argument.
Add some style changes. Inside the default.scss
file add:
Add some style changes. Inside the default.scss
file add:
And inside the home_page.scss
add:
And inside the home_page.scss
add:
The home page should look similar to this right now
The home page should look similar to this right now
Commit the changes
Commit the changes
git add -A
git commit -m "Add posts from all branches in the home page
- Modify the _main_content.html.erb file
- Define instance variables inside the PagesController’s index action
- Modify the no_posts_partial_path helper method to be more reusable
- Add CSS to style the home page"
Service objects (Service objects)
As I’ve mentioned before, if you put logic inside controllers, they become complicated very easily and a huge pain to test. That’s why it’s a good idea to extract logic from them somewhere else. To do that I use design patterns, service objects (services) to be more specific.
As I've mentioned before, if you put logic inside controllers, they become complicated very easily and a huge pain to test. That's why it's a good idea to extract logic from them somewhere else. To do that I use design patterns, service objects (services) to be more specific.
Right now inside the PostsController
, we have this method:
Right now inside the PostsController
, we have this method:
It has a lot of conditional logic which I want to remove by using services. Service objects (services) design pattern is just a basic ruby class. It’s very simple, we just pass data which we want to process and call a defined method to get a desired return value.
It has a lot of conditional logic which I want to remove by using services. Service objects (services) design pattern is just a basic ruby class . It's very simple, we just pass data which we want to process and call a defined method to get a desired return value.
In ruby we pass data to Class’s initialize
method, in other languages it’s known as the constructor
. And then inside the class, we just create a method which will handle all defined logic. Let’s create that and see how it looks in code.
In ruby we pass data to Class's initialize
method, in other languages it's known as the constructor
. And then inside the class, we just create a method which will handle all defined logic. Let's create that and see how it looks in code.
Inside the app
directory, create a new services
directory:
Inside the app
directory, create a new services
directory:
app/services
Inside the directory, create a new posts_for_branch_service.rb
file:
Inside the directory, create a new posts_for_branch_service.rb
file:
Here, as described above, it is just a plain ruby class with an initialize
method to accept parameters and a call
method to handle the logic. We took this logic from the get_posts
method.
Here, as described above, it is just a plain ruby class with an initialize
method to accept parameters and a call
method to handle the logic. We took this logic from the get_posts
method.
Now simply create a new object of this class and call the call
method inside the get_posts
method. The method should look like this right now:
Now simply create a new object of this class and call the call
method inside the get_posts
method. The method should look like this right now:
Commit the changes:
Commit the changes:
git add -A
git commit -m "Create a service object to extract logic
from the get_posts method"
Specs
Specs
A fortunate thing about design patterns, like services, is that it’s easy to write unit tests for it. We can simply write specs for the call
method and test each of its conditions.
A fortunate thing about design patterns, like services, is that it's easy to write unit tests for it. We can simply write specs for the call
method and test each of its conditions.
Inside the spec
directory create a new services
directory:
Inside the spec
directory create a new services
directory:
spec/services
Inside the directory create a new file posts_for_branch_service_spec.rb
Inside the directory create a new file posts_for_branch_service_spec.rb
At the top of the file, the posts_for_branch_service.rb
file is loaded and then each of the call
method’s conditions are tested.
At the top of the file, the posts_for_branch_service.rb
file is loaded and then each of the call
method's conditions are tested.
Commit the changes
Commit the changes
git add -A
git commit -m "Add specs for the PostsForBranchService"
Create a new post (Create a new post)
Until now posts were created artificially, by using seeds. Let’s add a user interface for it, so a user could create posts.
Until now posts were created artificially, by using seeds. Let's add a user interface for it, so a user could create posts.
Inside the posts_controller.rb
file add new
and create
actions.
Inside the posts_controller.rb
file add new
and create
actions.
Inside the new
action, we define some instance variables for the form to create new posts. Inside the @categories
instance variable, categories for a specific branch are stored. The @post
instance variable stores an object of a new post, this is needed for the Rails form.
Inside the new
action, we define some instance variables for the form to create new posts. Inside the @categories
instance variable, categories for a specific branch are stored. The @post
instance variable stores an object of a new post, this is needed for the Rails form.
Inside the create
action’s @post
instance variable, we create a new Post
object and fill it with data, using the post_params
method. Define this method within the private
scope:
Inside the create
action's @post
instance variable, we create a new Post
object and fill it with data, using the post_params
method. Define this method within the private
scope:
The permit
method is used to whitelist attributes of the object, so only these specified attributes are allowed to be passed.
The permit
method is used to whitelist attributes of the object, so only these specified attributes are allowed to be passed.
Also at the top of the PostsController
, add the following line:
Also at the top of the PostsController
, add the following line:
The before_action
is one of the Rails filters. We don’t want to allow for not signed in users to have an access to a page where they can create new posts. So before calling the new
action, the redirect_if_not_signed_in
method is called. We’ll need this method across other controllers too, so define it inside the application_controller.rb
file. Also a method to redirect signed in users would be useful in the future too. So define them both.
The before_action
is one of the Rails filters . We don't want to allow for not signed in users to have an access to a page where they can create new posts. So before calling the new
action, the redirect_if_not_signed_in
method is called. We'll need this method across other controllers too, so define it inside the application_controller.rb
file. Also a method to redirect signed in users would be useful in the future too. So define them both.
Now the new
template is required, so a user could create new posts. Inside the posts
directory, create a new.html.erb
file:
Now the new
template is required, so a user could create new posts. Inside the posts
directory, create a new.html.erb
file:
Create a new
directory and a _post_form.html.erb
partial file inside:
Create a new
directory and a _post_form.html.erb
partial file inside:
The form is pretty straightforward. Attributes of the fields are defined and the collection_select
method is used to allow to select one of the available categories.
The form is pretty straightforward. Attributes of the fields are defined and the collection_select
method is used to allow to select one of the available categories.
Commit the changes
Commit the changes
git add -A
git commit -m "Create a UI to create new posts
- Inside the PostsController:
define new and create actions
define a post_params method
define a before_action filter
- Inside the ApplicationController:
define a redirect_if_not_signed_in method
define a redirect_if_signed_in method
- Create a new template for posts"
We can test if the form works by writing specs. Start by writing request specs, to make sure that we get correct responses after we send particular requests. Inside the spec
directory create a couple directories.
We can test if the form works by writing specs. Start by writing request specs , to make sure that we get correct responses after we send particular requests. Inside the spec
directory create a couple directories.
spec/requests/posts
And a new_spec.rb
file inside:
And a new_spec.rb
file inside:
As mentioned in the documentation, request specs provide a thin wrapper around the integration tests. So we test if we get correct responses when we send certain requests. The include Warden::Test::Helpers
line is required in order to use login_as
method. The method logs a user in.
As mentioned in the documentation, request specs provide a thin wrapper around the integration tests. So we test if we get correct responses when we send certain requests. The include Warden::Test::Helpers
line is required in order to use login_as
method. The method logs a user in.
Commit the change.
Commit the change.
git add -A
git commit -m "Add request specs for a new post template"
We can even add some more request specs for the pages which we created previously.
We can even add some more request specs for the pages which we created previously.
Inside the same directory create a branches_spec.rb
file:
Inside the same directory create a branches_spec.rb
file:
This way we check that all branch pages’ templates successfully render. Also the shared_examples
is used to reduce the repetitive code.
This way we check that all branch pages' templates successfully render. Also the shared_examples
is used to reduce the repetitive code.
Commit the change.
Commit the change.
git add -A
git commit -m "Add request specs for Posts branch pages' templates"
Also we can make sure that the show
template renders successfully. Inside the same directory create a show_spec.rb
file:
Also we can make sure that the show
template renders successfully. Inside the same directory create a show_spec.rb
file:
Commit the changes.
Commit the changes.
git add -A
git commit -m "Add request specs for the Posts show template"
To make sure that a user is able to create a new post, write feature specs to test the form. Inside the features/posts
directory create a new file create_new_post_spec.rb
To make sure that a user is able to create a new post, write feature specs to test the form. Inside the features/posts
directory create a new file create_new_post_spec.rb
Commit the changes.
Commit the changes.
git add -A
git commit -m "Create a create_new_post_spec.rb file with feature specs"
Apply some design to the new
template.
Apply some design to the new
template.
Within the following directory:
Within the following directory:
assets/stylesheets/partials/posts
Create a new.scss
file:
Create a new.scss
file:
If you go to the template in a browser now, you should see a basic form
If you go to the template in a browser now, you should see a basic form
Commit the changes
Commit the changes
git add -A
git commit -m "Add CSS to the Posts new.html.erb template"
Finally, we want to make sure that all fields are filled correctly. Inside the Post
model we’re going to define some validations. Add the following code to the Post
model:
Finally, we want to make sure that all fields are filled correctly. Inside the Post
model we're going to define some validations . Add the following code to the Post
model:
Commit the changes.
Commit the changes.
git add -A
git commit -m "Add validations to the Post model"
Cover these validations with specs. Go to the Post
model’s spec file:
Cover these validations with specs. Go to the Post
model's spec file:
spec/models/post_spec.rb
Then add:
Then add:
Commit the changes.
Commit the changes.
git add -A
git commit -m "Add specs for the Post model's validations"
Merge the specific_branches
branch with the master
Merge the specific_branches
branch with the master
git checkout -b master
git merge specific_branches
git branch -D specific_branches
Instant Messaging (Instant Messaging)
Users are able to publish posts and read other users’ posts, but they have no ability to communicate with each other. We could create a simple mail box system, which would be much easier and faster to develop. But that is a very old way to communicate with someone. Real time communication is much more exciting to develop and comfortable to use.
Users are able to publish posts and read other users' posts, but they have no ability to communicate with each other. We could create a simple mail box system, which would be much easier and faster to develop. But that is a very old way to communicate with someone. Real time communication is much more exciting to develop and comfortable to use.
Fortunately, Rails has Action Cables which makes real time features’ implementation relatively easy. The core concept behind the Action Cables is that it uses a WebSockets Protocol instead of HTTP. And the core concept of WebSockets is that it establishes a client-server connection and keeps it open. This means that no page reloads are required to send and receive additional data.
Fortunately, Rails has Action Cables which makes real time features' implementation relatively easy. The core concept behind the Action Cables is that it uses a WebSockets Protocol instead of HTTP . And the core concept of WebSockets is that it establishes a client-server connection and keeps it open. This means that no page reloads are required to send and receive additional data.
Private conversation (Private conversation)
The goal of this section is to create a working feature which would allow to have a private conversation between two users.
The goal of this section is to create a working feature which would allow to have a private conversation between two users.
Switch to a new branch
Switch to a new branch
git checkout -B private_conversation
Namespacing models
Namespacing models
Start by defining necessary models. We’ll need two different models for now, one for private conversations and another for private messages. We could name them PrivateConversation
and PrivateMessage
, but you can quickly encounter a little problem. While everything would work fine, imagine how the models
directory would start to look like after we create more and more models with similar name prefixes. The directory would become hardly manageable in no time.
Start by defining necessary models. We'll need two different models for now, one for private conversations and another for private messages. We could name them PrivateConversation
and PrivateMessage
, but you can quickly encounter a little problem. While everything would work fine, imagine how the models
directory would start to look like after we create more and more models with similar name prefixes. The directory would become hardly manageable in no time.
To avoid chaotic structure inside directories, we can use a namespacing technique.
To avoid chaotic structure inside directories, we can use a namespacing technique.
Let’s see how it would look like. An ordinary model for private conversation would be called PrivateConversation
and its file would be called private_conversation.rb
, and stored inside the models
directory
Let's see how it would look like. An ordinary model for private conversation would be called PrivateConversation
and its file would be called private_conversation.rb
, and stored inside the models
directory
models/private_conversation.rb
Meanwhile, the namespaced version would be called Private::Conversation
. The file would be called conversation.rb
and located inside the private
directory
Meanwhile, the namespaced version would be called Private::Conversation
. The file would be called conversation.rb
and located inside the private
directory
models/private/conversation.rb
Can you see how it might be useful? All files with the private
prefix would be stored inside the private
directory, instead of accumulating inside the main models
directory and making it hardly to read.
Can you see how it might be useful? All files with the private
prefix would be stored inside the private
directory, instead of accumulating inside the main models
directory and making it hardly to read.
As usually, Rails makes the development process enjoyable. We’re able to create namespaced models by specifying a directory which we want to put a model in.
As usually, Rails makes the development process enjoyable. We're able to create namespaced models by specifying a directory which we want to put a model in.
To create the namespaced Private::Conversation
model run the following command:
To create the namespaced Private::Conversation
model run the following command:
rails g model private/conversation
Also generate the Private::Message
model:
Also generate the Private::Message
model:
rails g model private/message
If you look at the models
directory, you will see a private.rb
file. This is required to add prefix to database tables’ names, so models could be recognized. Personally I don’t like keeping those files inside the models
directory, I prefer to specify a table’s name inside a model itself. To specify a table’s name inside a model, you have to use self.table_name =
and provide a table’s name as a string. If you choose to specify names of database tables this way, like I do, then the models should look like this:
If you look at the models
directory, you will see a private.rb
file. This is required to add prefix to database tables' names, so models could be recognized. Personally I don't like keeping those files inside the models
directory, I prefer to specify a table's name inside a model itself. To specify a table's name inside a model, you have to use self.table_name =
and provide a table's name as a string. If you choose to specify names of database tables this way, like I do, then the models should look like this:
The private.rb
file, inside the models
directory, is no longer needed, you can delete it.
The private.rb
file, inside the models
directory, is no longer needed, you can delete it.
A user will be able to have many private conversations and conversations will have many messages. Define these associations inside the models:
A user will be able to have many private conversations and conversations will have many messages. Define these associations inside the models:
Here the class_name
method is used to define a name of an associated model. This allows to use custom names for our associations and make sure that namespaced models get recognized. Another use case of the class_name
method would be to create a relation to itself, this is useful when you want to differentiate same model’s data by creating some kind of hierarchies or similar structures.
Here the class_name
method is used to define a name of an associated model. This allows to use custom names for our associations and make sure that namespaced models get recognized. Another use case of the class_name
method would be to create a relation to itself, this is useful when you want to differentiate same model's data by creating some kind of hierarchies or similar structures.
The foreign_key
is used to specify a name of association’s column in a database table. A data column in a table is only created on the belongs_to
association’s side, but to make the column recognizable, we’ve to define the foreign_key
with same values on both models.
The foreign_key
is used to specify a name of association's column in a database table. A data column in a table is only created on the belongs_to
association's side, but to make the column recognizable, we've to define the foreign_key
with same values on both models.
Private conversations are going to be between two users, here these two users are sender
and recipient
. We could’ve named them like user1
and user2
. But it’s handy to know who initiated a conversation, so the sender
here is a creator of a conversation.
Private conversations are going to be between two users, here these two users are sender
and recipient
. We could've named them like user1
and user2
. But it's handy to know who initiated a conversation, so the sender
here is a creator of a conversation.
Define data tables inside the migration files:
Define data tables inside the migration files:
The private_conversations
table is going to store users’ ids, this is needed for belongs_to
and has_many
associations to work and of course to create a conversation between two users.
The private_conversations
table is going to store users' ids, this is needed for belongs_to
and has_many
associations to work and of course to create a conversation between two users.
Inside the body
data column, a message’s content is going to be stored. Instead of adding indexes and id columns to make associations between two models work, here we used the references
method, which simplified the implementation.
Inside the body
data column, a message's content is going to be stored. Instead of adding indexes and id columns to make associations between two models work, here we used the references
method, which simplified the implementation.
run migration files to create tables inside the development database
run migration files to create tables inside the development database
rails db:migrate
Commit the changes
Commit the changes
git add -A
git commit -m "Create Private::Conversation and Private::Message models
- Define associations between User, Private::Conversation
and Private::Message models
- Define private_conversations and private_messages tables"
A non-real time private conversation window
A non-real time private conversation window
We have a place to store data for private conversations, but that’s pretty much it. Where should we start from now? As mentioned in previous sections, personally I like to create a basic visual side of a feature and then write some logic to make it functional. I like this approach because when I have a visual element, which I want to make functional, it’s more obvious what I want to achieve. Once you have a user interface, it’s easier to start breaking down a problem into smaller steps, because you know what should happen after a certain event. It’s harder to program something that doesn’t exist yet.
We have a place to store data for private conversations, but that's pretty much it. Where should we start from now? As mentioned in previous sections, personally I like to create a basic visual side of a feature and then write some logic to make it functional. I like this approach because when I have a visual element, which I want to make functional, it's more obvious what I want to achieve. Once you have a user interface, it's easier to start breaking down a problem into smaller steps, because you know what should happen after a certain event. It's harder to program something that doesn't exist yet.
To start building the user interface for private conversations, create a Private::Conversations
controller. Once I namespace something in the app, I like to stay consistent and namespace all its other related parts too. This allows to understand and navigate through the source code more intuitively.
To start building the user interface for private conversations, create a Private::Conversations
controller. Once I namespace something in the app, I like to stay consistent and namespace all its other related parts too. This allows to understand and navigate through the source code more intuitively.
rails g controller private/conversations
Rails generator is pretty sweet. It created a namespaced model and namespaced views, everything is ready for development.
Rails generator is pretty sweet. It created a namespaced model and namespaced views, everything is ready for development.
Create a new conversation
Create a new conversation
We need a way to initiate a new conversation. In a case of our app, it makes sense that you want to contact a person which has similar interests to yours. A convenient place for this functionality is inside a single post’s page.
We need a way to initiate a new conversation. In a case of our app, it makes sense that you want to contact a person which has similar interests to yours. A convenient place for this functionality is inside a single post's page.
Inside the posts/show.html.erb
template, create a form to initiate a new conversation. Below the <%= @post.content %>
line add:
Inside the posts/show.html.erb
template, create a form to initiate a new conversation. Below the <%= @post.content %>
line add:
Define the helper method inside the posts_helper.rb
Define the helper method inside the posts_helper.rb
Add specs for the helper method:
Add specs for the helper method:
Create a show
directory and the corresponding partial files:
Create a show
directory and the corresponding partial files:
Define the leave_message_partial_path
helper method inside the posts_helper.rb
Define the leave_message_partial_path
helper method inside the posts_helper.rb
Add specs for the method
Add specs for the method
We’ll define the @message_has_been_sent
instance variable inside the PostsController
in just a moment, it will determine if an initial message to a user was already sent, or not.
We'll define the @message_has_been_sent
instance variable inside the PostsController
in just a moment, it will determine if an initial message to a user was already sent, or not.
Create partial files, corresponding to the leave_message_partial_path
helper method, inside a new contact_user
directory
Create partial files, corresponding to the leave_message_partial_path
helper method, inside a new contact_user
directory
Now configure the PostsController
’s show
action. Inside the action add
Now configure the PostsController
's show
action. Inside the action add
Within the controller’s private
scope, define the conversation_exist?
method
Within the controller's private
scope, define the conversation_exist?
方法
The between_users
method queries private conversations between two users. Define it as a scope inside the Private::Conversation
model
The between_users
method queries private conversations between two users. Define it as a scope inside the Private::Conversation
model
We have to test if the scope works. Before writing specs, define a private_conversation
factory, because we’ll need sample data inside the test database.
We have to test if the scope works. Before writing specs, define a private_conversation
factory, because we'll need sample data inside the test database.
We see a nested factory here, this allows to create a factory with its parent’s configuration and then modify it. Also because we’ll create messages with the private_conversation_with_messages
factory, we need to define the private_message
factory too
We see a nested factory here, this allows to create a factory with its parent's configuration and then modify it. Also because we'll create messages with the private_conversation_with_messages
factory, we need to define the private_message
factory too
Now we have everything ready to test the between_users
scope with specs.
Now we have everything ready to test the between_users
scope with specs.
Define the create
action for the Private::Conversations
controller
Define the create
action for the Private::Conversations
controller
Here we create a conversation between a post’s author and a current user. If everything goes well, the app will create a message, written by a current user, and give a feedback by rendering a corresponding JavaScript partial.
Here we create a conversation between a post's author and a current user. If everything goes well, the app will create a message, written by a current user, and give a feedback by rendering a corresponding JavaScript partial.
Create these partials
Create these partials
Create routes for the Private::Conversations
and Private::Messages
controllers
Create routes for the Private::Conversations
and Private::Messages
controllers
For now we’ll only need few actions, this is where the only
method is handy. The namespace
method allows to easily create routes for namespaced controllers.
For now we'll only need few actions, this is where the only
method is handy. The namespace
method allows to easily create routes for namespaced controllers.
Test the overall .contact-user
form’s performance with feature specs
Test the overall .contact-user
form's performance with feature specs
Commit the changes
Commit the changes
git add -A
git commit -m "Inside a post add a form to contact a user
- Define a contact_user_partial_path helper method in PostsHelper.
Add specs for the method
- Create _contact_user.html.erb and _login_required.html.erb partials
- Define a leave_message_partial_path helper method in PostsHelper.
Add specs for the method
- Create _already_in_touch.html.erb and _message_form.html.erb
partial files
- Define a @message_has_been_sent in PostsController's show action
- Define a between_users scope inside the Private::Conversation model
Add specs for the scope
- Define private_conversation and private_message factories
- Define routes for Private::Conversations and Private::Messages
- Define a create action inside the Private::Conversations
- Create _success.js and _fail.js partials
- Add feature specs to test the overall .contact-user form"
Change the form’s style a little bit by adding CSS to the branch_page.scss
file
Change the form's style a little bit by adding CSS to the branch_page.scss
file
When you visit a single post, the form should look something like this
When you visit a single post, the form should look something like this
When you send a message to a post’s author, the form disappears
When you send a message to a post's author, the form disappears
That’s how it looks like when you are already in touch with a user
That's how it looks like when you are already in touch with a user
Commit the changes
Commit the changes
git add -A
git commit -m "Add CSS to style the .contact-user form"
Render a conversation window
Render a conversation window
We sent a message and created a new conversation. That is the only our power right now, we cannot do anything else. What a useless power thus far. We need a conversation window to read and write messages.
We sent a message and created a new conversation. That is the only our power right now, we cannot do anything else. What a useless power thus far. We need a conversation window to read and write messages.
Store opened conversations’ ids inside the session. This allows to keep conversations opened in the app until a user closes them or destroys the session.
Store opened conversations' ids inside the session. This allows to keep conversations opened in the app until a user closes them or destroys the session.
Inside the Private::ConversationsController
’s create
action call a add_to_conversations unless already_added?
method if a conversation is successfully saved. Then define the method within the private
scope
Inside the Private::ConversationsController
's create
action call a add_to_conversations unless already_added?
method if a conversation is successfully saved. Then define the method within the private
scope
This will store the conversation’s id inside the session. And the already_added?
private method is going to make sure that the conversation’s id isn’t added inside the session yet.
This will store the conversation's id inside the session. And the already_added?
private method is going to make sure that the conversation's id isn't added inside the session yet.
And lastly, we’ll need an access to the conversation inside the views, so convert the conversation
variable into an instance variable.
And lastly, we'll need an access to the conversation inside the views, so convert the conversation
variable into an instance variable.
Now we can start building a template for the conversation window. Create a partial file for the window
Now we can start building a template for the conversation window. Create a partial file for the window
Here we get the conversation’s recipient with the private_conv_recipient
method. Define the helper method inside the Private::ConversationsHelper
Here we get the conversation's recipient with the private_conv_recipient
method. Define the helper method inside the Private::ConversationsHelper
The opposed_user
method is used. Go to the Private::Conversation
model and define the method
The opposed_user
method is used. Go to the Private::Conversation
model and define the method
This will return an opposed user of a private conversation. Make sure that the method works correctly by covering it with specs
This will return an opposed user of a private conversation. Make sure that the method works correctly by covering it with specs
Next, create missing partial files for the _conversation.html.erb
file
Next, create missing partial files for the _conversation.html.erb
file
Inside the Private::ConversationsHelper
, define the load_private_messages
helper method
Inside the Private::ConversationsHelper
, define the load_private_messages
helper method
This will add a link to load previous messages. Create a corresponding partial file inside a new messages_list
directory
This will add a link to load previous messages. Create a corresponding partial file inside a new messages_list
directory
Don’t forget to make sure that everything is fine with the method and write specs for it
Don't forget to make sure that everything is fine with the method and write specs for it
Because conversations’ windows are going to be rendered throughout the whole app, it means we’ll need an access to Private::ConversationsHelper
helper methods. To have an access to all these methods across the whole app, inside the ApplicationHelper
add
Because conversations' windows are going to be rendered throughout the whole app, it means we'll need an access to Private::ConversationsHelper
helper methods. To have an access to all these methods across the whole app, inside the ApplicationHelper
add
include Private::ConversationsHelper
Then create the last missing partial file for the conversation’s new message form
Then create the last missing partial file for the conversation's new message form
We’ll make this form functional a little bit later.
We'll make this form functional a little bit later.
Now let’s create a feature that after a user sends a message through an individual post, the conversation window gets rendered on the app.
Now let's create a feature that after a user sends a message through an individual post, the conversation window gets rendered on the app.
Inside the _success.js.erb
file
Inside the _success.js.erb
file
posts/show/contact_user/message_form/_success.js.erb
add
加
<%= render 'private/conversations/open' %>
This partial file’s purpose is to add a conversation window to the app. Define the partial file
This partial file's purpose is to add a conversation window to the app. Define the partial file
This callback partial file is going to be reused in multiple scenarios. To avoid rendering the same window multiple times, before rendering a window we check if it already exists on the app. Then we expand the window and auto focus the message form. At the bottom of the file, the positionChatWindows()
function is called to make sure that all conversations’ windows are well positioned. If we didn’t position them, they would just be rendered at the same spot, which of course would be unusable.
This callback partial file is going to be reused in multiple scenarios. To avoid rendering the same window multiple times, before rendering a window we check if it already exists on the app. Then we expand the window and auto focus the message form. At the bottom of the file, the positionChatWindows()
function is called to make sure that all conversations' windows are well positioned. If we didn't position them, they would just be rendered at the same spot, which of course would be unusable.
Now in the assets
directory create a file which will take care of the conversations’ windows visibility and positioning
Now in the assets
directory create a file which will take care of the conversations' windows visibility and positioning
Instead of creating our own functions for setting and getting cookies or similar way to manage data between JavaScript, we can use the gon gem. An original usage of this gem is to send data from the server side to JavaScript. But I also find it useful for keeping track of JavaScript variables across the app. Install and set up the gem by reading the instructions.
Instead of creating our own functions for setting and getting cookies or similar way to manage data between JavaScript, we can use the gon gem. An original usage of this gem is to send data from the server side to JavaScript. But I also find it useful for keeping track of JavaScript variables across the app. Install and set up the gem by reading the instructions.
We keep track of the viewport’s width with an event listener. When a conversation gets close to the viewport’s left side, the conversation gets hidden. Once there is enough of free space for a hidden conversation window, the app displays it again.
We keep track of the viewport's width with an event listener. When a conversation gets close to the viewport's left side, the conversation gets hidden. Once there is enough of free space for a hidden conversation window, the app displays it again.
On a page visit we call the positioning and visibility functions to make sure that all conversations’ windows are in right positions.
On a page visit we call the positioning and visibility functions to make sure that all conversations' windows are in right positions.
We’re using the bootstrap’s panel component to easily expand and collapse conversations’ windows. By default they are going to be collapsed and not interactive at all. To make them toggleable, inside the javascripts
directory create a new file toggle_window.js
We're using the bootstrap's panel component to easily expand and collapse conversations' windows. By default they are going to be collapsed and not interactive at all. To make them toggleable, inside the javascripts
directory create a new file toggle_window.js
Create a new conversation_window.scss
file
Create a new conversation_window.scss
file
assets/stylesheets/partials/conversation_window.scss
And add CSS to style conversations’ windows
And add CSS to style conversations' windows
You might noticed that there are some classes that haven’t been defined yet in any HTML file. That’s because the future files, we’ll create in the views
directory, are going to have shared CSS with already existent HTML elements. Instead of jumping back and forth to CSS files multiple times after we add any minor HTML element, I have included some classes, defined in future HTML elements, right now. Remember, you can always go to style sheets and analyze how a particular styling works.
You might noticed that there are some classes that haven't been defined yet in any HTML file. That's because the future files, we'll create in the views
directory, are going to have shared CSS with already existent HTML elements. Instead of jumping back and forth to CSS files multiple times after we add any minor HTML element, I have included some classes, defined in future HTML elements, right now. Remember, you can always go to style sheets and analyze how a particular styling works.
Previously we’ve saved an id of a newly created conversation inside the session. It’s time to take an advantage of it and keep the conversation window opened until a user closes it or destroys the session. Inside the ApplicationController
define a filter
Previously we've saved an id of a newly created conversation inside the session. It's time to take an advantage of it and keep the conversation window opened until a user closes it or destroys the session. Inside the ApplicationController
define a filter
before_action :opened_conversations_windows
and then define the opened_conversations_windows
method
and then define the opened_conversations_windows
method
The includes
method is used to include the data from associated database tables. In the near future we’ll load messages from a conversation. If we didn’t use the includes
method, we wouldn’t have loaded messages records of a conversation with this query. This would lead to a N + 1 query problem. If we didn’t load messages with the query, an additional query would be fired for every message. This would significantly impact performance of the app. Now instead of 100 queries for 100 messages, we have an only one initial query for any number of messages.
The includes
method is used to include the data from associated database tables. In the near future we'll load messages from a conversation. If we didn't use the includes
method, we wouldn't have loaded messages records of a conversation with this query. This would lead to a N + 1 query problem. If we didn't load messages with the query, an additional query would be fired for every message. This would significantly impact performance of the app. Now instead of 100 queries for 100 messages, we have an only one initial query for any number of messages.
Inside the application.html.erb
file, just below the yield
method, add
Inside the application.html.erb
file, just below the yield
method, add
Create a new application
directory and inside create the _private_conversations_windows.html.erb
partial file
Create a new application
directory and inside create the _private_conversations_windows.html.erb
partial file
Now when we browse through the app, we see opened conversations all the time, no matter what page we are in.
Now when we browse through the app, we see opened conversations all the time, no matter what page we are in.
Commit the changes
Commit the changes
git add -A
git commit -m "Render a private conversation window on the app
- Add opened conversations to the session
- Create a _conversation.html.erb file inside private/conversations
- Define a private_conv_recipient helper method in the
private/conversations_helper.rb
- Define an opposed_user method in Private::Conversation model
and add specs for it
- Create _heading.html.erb and _messages_list.html.erb files
inside the private/conversations/conversation
- Define a load_private_messages in private/conversations_helper.rb
and add specs for it
- Create a _new_message_form.html.erb inside the
private/conversations/conversation
- Create a _open.js.erbinside private/conversations
- Create a position_and_visibility.js inside the
assets/javascripts/conversations
- Create a conversation_window.scss inside the
assets/stylesheets/partials
- Define an opened_conversations_windows helper method in
ApplicationController
- Create a _private_conversations_windows.html.erb inside the
layouts/application
Close a conversation
Close a conversation
The conversation’s close button isn’t functional yet. But we have everything ready to make it so. Inside the Private::ConversationsController
, define a close
action
The conversation's close button isn't functional yet. But we have everything ready to make it so. Inside the Private::ConversationsController
, define a close
action
When the close button is clicked, this action will be called. The action deletes conversation’s id from the session and then responds with a js partial file, identical to the action’s name. Create the partial file
When the close button is clicked, this action will be called. The action deletes conversation's id from the session and then responds with a js partial file, identical to the action's name. Create the partial file
It removes the conversation’s window from the DOM and re-positions the rest of conversations’ windows.
It removes the conversation's window from the DOM and re-positions the rest of conversations' windows.
Commit the changes
Commit the changes
git add -A
git commit -m "Make the close conversation button functional
- Define a close action inside the Private::ConversationsController
- Create a close.js.erb inside the private/conversations"
Render messages
Render messages
Currently in the messages list we see a loading icon without any messages. That’s because we haven’t created any templates for messages. Inside the views/private
directory, create a messages
directory. Inside the directory, create a new file
Currently in the messages list we see a loading icon without any messages. That's because we haven't created any templates for messages. Inside the views/private
directory, create a messages
directory. Inside the directory, create a new file
The private_message_date_check
helper method checks if this message is written at the same day as a previous message. If not, it renders an extra line with a new date. Define the helper method inside the Private::MessagesHelper
The private_message_date_check
helper method checks if this message is written at the same day as a previous message. If not, it renders an extra line with a new date. Define the helper method inside the Private::MessagesHelper
Inside the ApplicationHelper
, include the Private::MessagesHelper
, so we could have an access to it across the app
Inside the ApplicationHelper
, include the Private::MessagesHelper
, so we could have an access to it across the app
include Private::MessagesHelper
Write specs for the method. Create a new messages_helper_spec.rb
file
Write specs for the method. Create a new messages_helper_spec.rb
file
Inside a new message
directory, create a _new_date.html.erb
file
Inside a new message
directory, create a _new_date.html.erb
file
Then inside the _message.html.erb
file, we have sent_or_received
and seen_or_unseen
helper methods. They return different classes in different cases. Define them inside the Private::MessagesHelper
Then inside the _message.html.erb
file, we have sent_or_received
and seen_or_unseen
helper methods. They return different classes in different cases. Define them inside the Private::MessagesHelper
Write specs for them:
Write specs for them:
Now we need a component to load messages into the messages list. Also this component is going to add previous messages at the top of the list, when a user scrolls up, until there is no messages left in a conversation. We are going to have an infinite scroll mechanism for messages, similar to the one we have in posts’ pages.
Now we need a component to load messages into the messages list. Also this component is going to add previous messages at the top of the list, when a user scrolls up, until there is no messages left in a conversation. We are going to have an infinite scroll mechanism for messages, similar to the one we have in posts' pages.
Inside the views/private/messages
directory create a _load_more_messages.js.erb
file:
Inside the views/private/messages
directory create a _load_more_messages.js.erb
file:
The @id_type
instance variable determines a type of the conversation. In the future we will be able to create not only private conversations, but group too. This leads to common helper methods and partial files between both types.
The @id_type
instance variable determines a type of the conversation. In the future we will be able to create not only private conversations, but group too. This leads to common helper methods and partial files between both types.
Inside the helpers
directory, create a shared
directory. Create a messages_helper.rb
file and define a helper method
Inside the helpers
directory, create a shared
directory. Create a messages_helper.rb
file and define a helper method
So far the method is pretty dumb. It just returns a partial’s path. We’ll give some intelligence to it later, when we’ll build extra features to our messaging system. Right now we won’t have an access to helper methods, defined in this file, in any other file. We have to include them inside other helper files. Inside thePrivate::MessagesHelper
, include methods from the Shared::MessagesHelper
So far the method is pretty dumb. It just returns a partial's path. We'll give some intelligence to it later, when we'll build extra features to our messaging system. Right now we won't have an access to helper methods, defined in this file, in any other file. We have to include them inside other helper files. Inside the Private::MessagesHelper
, include methods from the Shared::MessagesHelper
require 'shared/messages_helper'
include Shared::MessagesHelper
Inside the shared directory, create few new directories:
Inside the shared directory, create few new directories:
shared/load_more_messages/window
Then create an _append_messages.js.erb file:
Then create an _append_messages.js.erb file:
This code takes care that previous messages get appended to the top of the messages list. Then define another, again, not that fascinating, helper method inside the Private::MessagesHelper
This code takes care that previous messages get appended to the top of the messages list. Then define another, again, not that fascinating, helper method inside the Private::MessagesHelper
Create the corresponding directories inside the private/messages
directory and create a _add_link_to_messages.js.erb
file
Create the corresponding directories inside the private/messages
directory and create a _add_link_to_messages.js.erb
file
This file is going to update the link which loads previous messages. After previous messages are appended, the link is replaced with an updated link to load older previous messages.
This file is going to update the link which loads previous messages. After previous messages are appended, the link is replaced with an updated link to load older previous messages.
Now we have all this system, how previous messages get appended to the top of the messages list. But, if we tried to go to the app and opened a conversation window, we wouldn’t see any rendered messages. Why? Because nothing triggers the link to load previous messages. When we open a conversation window for the first time, we want to see the most recent messages. We can program the conversation window in a way that once it gets expanded, the load more messages link gets triggered, to load the most recent messages. It initiates the first cycle of appending previous messages and replacing the load more messages link with an updated one.
Now we have all this system, how previous messages get appended to the top of the messages list. But, if we tried to go to the app and opened a conversation window, we wouldn't see any rendered messages. 为什么? Because nothing triggers the link to load previous messages. When we open a conversation window for the first time, we want to see the most recent messages. We can program the conversation window in a way that once it gets expanded, the load more messages link gets triggered, to load the most recent messages. It initiates the first cycle of appending previous messages and replacing the load more messages link with an updated one.
Inside the toggle_window.js
file update the toggle
function to do exactly what is described above
Inside the toggle_window.js
file update the toggle
function to do exactly what is described above
Create an event handler, so whenever a user scrolls up and reaches almost the top of the messages list, the load more messages link will be triggered.
Create an event handler, so whenever a user scrolls up and reaches almost the top of the messages list, the load more messages link will be triggered.
When the load more messages link is going to be clicked, a Private::MessagesController
's index
action gets called. That’s the path, we defined to the load previous messages link. Create the controller and its index
action
When the load more messages link is going to be clicked, a Private::MessagesController
's index
action gets called. That's the path, we defined to the load previous messages link. Create the controller and its index
action
Here we include methods from the Messages
module. The module is stored inside the concerns
directory. ActiveSupport::Concern is one of the places, where you can store modules which you can later use in classes. In our case we include extra methods to our controller from the module. The get_messages
method comes from the Messages
module. The reason why it is stored inside the module is that we’ll use this exact same method in another controller a little bit later. To avoid code duplication, we make the method more reusable.
Here we include methods from the Messages
module. The module is stored inside the concerns
directory. ActiveSupport::Concern is one of the places, where you can store modules which you can later use in classes. In our case we include extra methods to our controller from the module. The get_messages
method comes from the Messages
module. The reason why it is stored inside the module is that we'll use this exact same method in another controller a little bit later. To avoid code duplication, we make the method more reusable.
I’ve seen some people complaining about the ActiveSupport::Concern
and suggest not to use it at all. I challenge those people to fight me in the octagon. I’m kidding :D. This is an independent application and we can create our app however we like it. If you don’t like concerns
, there are bunch of other ways to create reusable methods.
I've seen some people complaining about the ActiveSupport::Concern
and suggest not to use it at all. I challenge those people to fight me in the octagon. I'm kidding :D. This is an independent application and we can create our app however we like it. If you don't like concerns
, there are bunch of other ways to create reusable methods.
Create the module
Create the module
Here we require the active_support/concern
and then extend our module with ActiveSupport::Concern
, so Rails knows that it is a concern.
Here we require the active_support/concern
and then extend our module with ActiveSupport::Concern
, so Rails knows that it is a concern.
With the constantize
method we dynamically create a constant name by inputting a string value. We call models dynamically. The same method is going to be used for Private::Conversation
and Group::Conversation
models.
With the constantize
method we dynamically create a constant name by inputting a string value. We call models dynamically. The same method is going to be used for Private::Conversation
and Group::Conversation
models.
After the get_messages
method sets all necessary instance variables, the index
action responds with the _load_more_messages.js.erb
partial file.
After the get_messages
method sets all necessary instance variables, the index
action responds with the _load_more_messages.js.erb
partial file.
Finally, after messages get appended to the top of the messages list, we want to remove the loading icon from the conversation window. At the bottom of the _load_more_messages.js.erb
file add
Finally, after messages get appended to the top of the messages list, we want to remove the loading icon from the conversation window. At the bottom of the _load_more_messages.js.erb
file add
<%= render remove_link_to_messages %>
Now define the remove_link_to_messages
helper method inside the Shared::MessagesHelper
Now define the remove_link_to_messages
helper method inside the Shared::MessagesHelper
Try to write specs for the method on your own.
Try to write specs for the method on your own.
Create the _remove_more_messages_link.js.erb
partial file
Create the _remove_more_messages_link.js.erb
partial file
Now in a case, where are no previous messages left, the link to previous messages and the loading icon will be removed.
Now in a case, where are no previous messages left, the link to previous messages and the loading icon will be removed.
If you try to contact a user now, a conversation window will be rendered with a message, you sent, inside. We’re able to render messages via AJAX requests.
If you try to contact a user now, a conversation window will be rendered with a message, you sent, inside. We're able to render messages via AJAX requests.
Commit the changes.
Commit the changes.
git add -A
git commit -m "Render messages with AJAX
- Create a _message.html.erb inside private/messages
- Define a private_message_date_check helper method in
Private::MessagesHelper and write specs for it
- Create a _new_date.html.erb inside private/messages/message
- Define sent_or_received and seen_or_unseen helper methods in
Private::MessagesHelper and write specs for them
- Create a _load_more_messages.js.erb inside private/messages
- Define an append_previous_messages_partial_path helper method in
Shared::MessagesHelper
- Create a _append_messages.js.erb inside
shared/load_more_messages/window
- Define a replace_link_to_private_messages_partial_path in
Private::MessagesHelper
- Create a _add_link_to_messages.js.erb inside
private/messages/load_more_messages/window
- Create a toggle_window.js inside javascripts/conversations
- Create a messages_infinite_scroll.js inside
assets/javascripts/conversations
- Define an index action inside the Private::MessagesController
- Create a messages.rb inside controllers/concerns
- Define a remove_link_to_messages inside helpers/shared
- Create a _remove_more_messages_link.js.erb inside
shared/load_more_messages/window"
Real time functionality with Action Cable (Real time functionality with Action Cable)
Conversations’ windows look pretty neat already. And they also have some sweet functionality. But, they are lacking the most important feature — ability to send and receive messages in real time.
Conversations' windows look pretty neat already. And they also have some sweet functionality. But, they are lacking the most important feature — ability to send and receive messages in real time.
As briefly discussed previously, Action Cable will allow us to achieve the desired real time feature for conversations. You should skim through the documentation to be aware how it all works.
As briefly discussed previously, Action Cable will allow us to achieve the desired real time feature for conversations. You should skim through the documentation to be aware how it all works.
The first thing which we should do is create a WebSocket connection and subscribe to a specific channel. Luckily, WebSocket connections are already covered by default Rails configuration. Inside the app/channels/application_cable
directory you see channel.rb
and connection.rb
files. The Connection
class takes care of the authentication and the Channel
class is a parent class to store shared logic between all channels.
The first thing which we should do is create a WebSocket connection and subscribe to a specific channel. Luckily, WebSocket connections are already covered by default Rails configuration. Inside the app/channels/application_cable
directory you see channel.rb
and connection.rb
files. The Connection
class takes care of the authentication and the Channel
class is a parent class to store shared logic between all channels.
Connection is set by default. Now we need a private conversation channel to subscribe to. Generate a namespaced channel
Connection is set by default. Now we need a private conversation channel to subscribe to. Generate a namespaced channel
rails g channel private/conversation
Inside the generated Private::ConversationChannel
, we see subscribed
and unsubscribed
methods. With the subscribed
method a user creates a connection to the channel. With the unsubscribed
method a user, obviously, destroys the connection.
Inside the generated Private::ConversationChannel
, we see subscribed
and unsubscribed
methods. With the subscribed
method a user creates a connection to the channel. With the unsubscribed
method a user, obviously, destroys the connection.
Update those methods:
Update those methods:
Here we want that a user would have its own unique channel. From the channel a user will receive and send data. Because users’ ids are unique, we make the channel unique by adding a user’s id.
Here we want that a user would have its own unique channel. From the channel a user will receive and send data. Because users' ids are unique, we make the channel unique by adding a user's id.
This is a server side connection. Now we need to create a connection on the client side too.
This is a server side connection. Now we need to create a connection on the client side too.
To create an instance of the connection on the client side, we have to write some JavaScript. Actually, Rails has already created it with the channel generator. Navigate to assets/javascripts/channels/private
and by default Rails generates CoffeeScript
files. I’m going to use JavaScript here. So rename the file to conversation.js
and replace its content with:
To create an instance of the connection on the client side, we have to write some JavaScript. Actually, Rails has already created it with the channel generator. Navigate to assets/javascripts/channels/private
and by default Rails generates CoffeeScript
files. I'm going to use JavaScript here. So rename the file to conversation.js
and replace its content with:
Restart the server, go to the app, login and check the server log.
Restart the server, go to the app, login and check the server log.
We got the connection. The core of the real time communication is set. We’ve a constantly open client-server connection. It means that we can send and receive data from the server without restarting the connection or refreshing a browser, man! A really powerful thing when you think about it. From now we’ll build the messaging system around this connection.
We got the connection. The core of the real time communication is set. We've a constantly open client-server connection. It means that we can send and receive data from the server without restarting the connection or refreshing a browser, man! A really powerful thing when you think about it. From now we'll build the messaging system around this connection.
Commit the changes.
Commit the changes.
git add -A
git commit -m "Create a unique private conversation channel and subscribe to it"
Let’s make the conversation window’s new message form functional. At the bottom of the assets/javascripts/channels/private/conversations.js
file add this function:
Let's make the conversation window's new message form functional. At the bottom of the assets/javascripts/channels/private/conversations.js
file add this function:
The function is going to get values from the new message form and pass them to a send_message
function. The send_message
function is going to call a send_message
method on the server side, which will take care of creating a new message.
The function is going to get values from the new message form and pass them to a send_message
function. The send_message
function is going to call a send_message
method on the server side, which will take care of creating a new message.
Also take a note, the event handler is on a submit button, but on the conversation window we don’t have any visible submit buttons. It’s a design choice. We have to program the conversation window in a way that submit button is triggered when the enter key is clicked on a keyboard. This function is going to be used in the future by other features, so create a conversation.js
file inside the assets/javascripts/conversations
directory
Also take a note, the event handler is on a submit button, but on the conversation window we don't have any visible submit buttons. It's a design choice. We have to program the conversation window in a way that submit button is triggered when the enter key is clicked on a keyboard. This function is going to be used in the future by other features, so create a conversation.js
file inside the assets/javascripts/conversations
directory
In the file we describe some general behavior for conversations’ windows. The first behavior is to keep the scrollbar away from the top, so previous messages aren’t loaded when it is not needed. The second function makes sure that the submit button is triggered on the enter key click and then cleans input’s value back to an empty string.
In the file we describe some general behavior for conversations' windows. The first behavior is to keep the scrollbar away from the top, so previous messages aren't loaded when it is not needed. The second function makes sure that the submit button is triggered on the enter key click and then cleans input's value back to an empty string.
Start by creating the send_message
function inside the private_conversation
object. Add it below the received
callback function
Start by creating the send_message
function inside the private_conversation
object. Add it below the received
callback function
This calls the send_message
method on the server side and passes the message value. The server side method should be defined inside the Private::ConversationChannel
. Define the method:
This calls the send_message
method on the server side and passes the message value. The server side method should be defined inside the Private::ConversationChannel
. Define the method:
This will take care of a new message’s creation. The data
parameter, which we get from the passed argument, is a nested hash. So to reduce this nested complexity into a single hash, the each_with_object
method is used.
This will take care of a new message's creation. The data
parameter, which we get from the passed argument, is a nested hash. So to reduce this nested complexity into a single hash, the each_with_object
method is used.
If you try to send a new message inside a conversation’s window, a new message record will actually be created. It won’t show up on the conversation window instantly yet, only when you refresh the website. It would show up, but we haven’t set anything to broadcast newly created messages to a private conversation’s channel. We’ll implement it in just a moment. But before we continue and commit changes, quickly recap how the current messaging system works.
If you try to send a new message inside a conversation's window, a new message record will actually be created. It won't show up on the conversation window instantly yet, only when you refresh the website. It would show up, but we haven't set anything to broadcast newly created messages to a private conversation's channel. We'll implement it in just a moment. But before we continue and commit changes, quickly recap how the current messaging system works.
- A user fills the new message form and submits the message A user fills the new message form and submits the message
The event handler inside the javascripts/channels/private/conversations.js
gets a conversation window’s data, a conversation id and a message value, and triggers the channel instances on the client-side send_message
function.
The event handler inside the javascripts/channels/private/conversations.js
gets a conversation window's data, a conversation id and a message value, and triggers the channel instances on the client-side send_message
function.
3. The send_message
function on the client side calls the send_message
method on the server side and passes data to it
3. The send_message
function on the client side calls the send_message
method on the server side and passes data to it
4. The send_message
method on the client side processes provided data and creates a new Private::Message
record
4. The send_message
method on the client side processes provided data and creates a new Private::Message
record
Commit the changes.
Commit the changes.
git add -A
git commit -m "Make a private conversation window's new message form functional
- Add an event handler inside the
javascripts/channels/private/conversation.js to trigger the submit button
- Define a common behavior among conversation windows inside the
assets/javascripts/conversations/conversation.js
- Define a send_message function on both, client and server, sides"
Broadcast a new message
Broadcast a new message
After a new message is created, we want to broadcast it to a corresponding channel somehow. Well, Active Record Callbacks arms us with plenty of useful callback methods for models. There is a after_create_commit
callback method, which runs whenever a new model’s record gets created. Inside the Private::Message
model’s file add
After a new message is created, we want to broadcast it to a corresponding channel somehow. Well, Active Record Callbacks arms us with plenty of useful callback methods for models. There is a after_create_commit
callback method, which runs whenever a new model's record gets created. Inside the Private::Message
model's file add
As you see, after a record’s creation, the Private::MessageBroadcastJob.perform_later
gets called. And what’s that? It’s a background job, handling back-end operations. It allows to run certain operations whenever we want to. It could be immediately after a particular event, or be scheduled to run some time later after an event. If you aren’t familiar with background jobs, checkout Active Job Basics.
As you see, after a record's creation, the Private::MessageBroadcastJob.perform_later
gets called. 那是什么 It's a background job, handling back-end operations. It allows to run certain operations whenever we want to. It could be immediately after a particular event, or be scheduled to run some time later after an event. If you aren't familiar with background jobs, checkout Active Job Basics .
Add specs for the previous_message
method. If you are going to try run specs now, comment out the after_create_commit
method. We haven’t defined the Private::MessageBroadcastJob
, so currently specs would raise an undefined constant error.
Add specs for the previous_message
method. If you are going to try run specs now, comment out the after_create_commit
method. We haven't defined the Private::MessageBroadcastJob
, so currently specs would raise an undefined constant error.
Now we can create a background job which will broadcast a newly created message to a private conversation’s channel.
Now we can create a background job which will broadcast a newly created message to a private conversation's channel.
rails g job private/message_broadcast
Inside the file we see a perform
method. By default, when you call a job, this method is called. Now inside the job, process the given data and broadcast it to channel’s subscribers.
Inside the file we see a perform
method. By default, when you call a job, this method is called. Now inside the job, process the given data and broadcast it to channel's subscribers.
Here we render a message and send it to both channel’s subscribers. Also we pass some additional key-value pairs to properly display the message. If we tried to send a new message, users would receive data, but the message wouldn’t be appended to the messages list. No visible changes would be made.
Here we render a message and send it to both channel's subscribers. Also we pass some additional key-value pairs to properly display the message. If we tried to send a new message, users would receive data, but the message wouldn't be appended to the messages list. No visible changes would be made.
When data is broadcasted to a channel, the received
callback function on the client side gets called. This is where we have an opportunity to append data to the DOM. Inside the received
function add the following code:
When data is broadcasted to a channel, the received
callback function on the client side gets called. This is where we have an opportunity to append data to the DOM. Inside the received
function add the following code:
Here we see that the sender and the recipient get treated a little bit differently.
Here we see that the sender and the recipient get treated a little bit differently.
// change style of conv window when there are unseen messages
// add an additional class to the conversation's window or something
I’ve created this intentionally, so whenever a conversation has unseen messages, you could style its window however you like it. You can change a window’s color, make it blink, or whatever you want to.
I've created this intentionally, so whenever a conversation has unseen messages, you could style its window however you like it. You can change a window's color, make it blink, or whatever you want to.
Also there are findConv
, ConvRendered
, ConvMessagesVisibility
functions used. We’ll use these functions for both type of chats, private and group.
Also there are findConv
, ConvRendered
, ConvMessagesVisibility
functions used. We'll use these functions for both type of chats, private and group.
Create a shared
directory:
Create a shared
directory:
assets/javascripts/channels/shared
Create a conversation.js
file inside this directory.
Create a conversation.js
file inside this directory.
A messenger is mentioned in the code quite a lot and we don’t have the messenger yet. The messenger is going to be a separate way to open conversations. To prevent a lot of small changes in the future, I’ve included cases with the messenger right now.
A messenger is mentioned in the code quite a lot and we don't have the messenger yet. The messenger is going to be a separate way to open conversations. To prevent a lot of small changes in the future, I've included cases with the messenger right now.
That’s it, the real time functionality should work. Both users, the sender and the recipient, should receive and get displayed new messages on the DOM. When we send a new message, we should see it instantly appended to the messages list. But there’s one little problem now. We only have a one way to render a conversation window. It gets rendered only when a conversation is created. We’ll add additional ways to render conversations’ windows in just a moment. But before that, let’s recap how data reaches channel’s subscribers.
That's it, the real time functionality should work. Both users, the sender and the recipient, should receive and get displayed new messages on the DOM. When we send a new message, we should see it instantly appended to the messages list. But there's one little problem now. We only have a one way to render a conversation window. It gets rendered only when a conversation is created. We'll add additional ways to render conversations' windows in just a moment. But before that, let's recap how data reaches channel's subscribers.
After a new Private::Message
record is created, the after_create_commit
method gets triggered, which calls the background job
After a new Private::Message
record is created, the after_create_commit
method gets triggered, which calls the background job
Private::MessageBroadcastJob
processes given data and broadcasts it to channel’s subscribers
Private::MessageBroadcastJob
processes given data and broadcasts it to channel's subscribers
On the client side the received
callback function is called, which appends data to the DOM
On the client side the received
callback function is called, which appends data to the DOM
Commit the changes.
Commit the changes.
git add -A
git commit -m "Broadcast a new message
- Inside the Private::Message define an after_create_comit callback method.
- Create a Private::MessageBroadcastJob
- Define a received function inside the
assets/javascripts/channels/private/conversation.js
- Create a conversation.js inside the
assets/javascripts/channels/shared"
Navigation bar update (Navigation bar update)
On the navigation bar we’re going to render a list of user’s conversations. When a list of conversations is opened, we want to see conversations ordered by the latest messages. Conversations with the most recent messages are going to be at the top of the list. This list should be accessible throughout the whole application. So inside the ApplicationController
, store ordered user’s conversations inside an instance variable. The way I suggest doing that is define an all_ordered_conversations
method inside the controller
On the navigation bar we're going to render a list of user's conversations. When a list of conversations is opened, we want to see conversations ordered by the latest messages. Conversations with the most recent messages are going to be at the top of the list. This list should be accessible throughout the whole application. So inside the ApplicationController
, store ordered user's conversations inside an instance variable. The way I suggest doing that is define an all_ordered_conversations
method inside the controller
Add a before_action
filter, so the @all_conversations
instance variable is available everywhere.
Add a before_action
filter, so the @all_conversations
instance variable is available everywhere.
before_action :all_ordered_conversations
And then create an OrderConversationsService
to take care of conversations’ querying and ordering.
And then create an OrderConversationsService
to take care of conversations' querying and ordering.
Currently this service only deals with private conversations, that’s the only type of conversations we’ve developed so far. In the future we’ll mash private and group conversations together, and sort them by their latest messages. The sort
method is used to sort an array of conversations. Again, if we didn’t use the includes
method, we would experience a N + 1 query problem. Because when we sort conversations, we check the latest messages’ creation dates of every conversation and compare them. That’s why with the query we have included messages’ records.
Currently this service only deals with private conversations, that's the only type of conversations we've developed so far. In the future we'll mash private and group conversations together, and sort them by their latest messages. The sort
method is used to sort an array of conversations. Again, if we didn't use the includes
method, we would experience a N + 1 query problem. Because when we sort conversations, we check the latest messages' creation dates of every conversation and compare them. That's why with the query we have included messages' records.
The <=>
operator evaluates which created_at
value is higher. If we used a <=> b
, it would sort a given array in ascending order. When you evaluate values in the opposite way, b <=> a
, it sorts an array in descending order.
The <=>
operator evaluates which created_at
value is higher. If we used a <=> b
, it would sort a given array in ascending order. When you evaluate values in the opposite way, b <=> a
, it sorts an array in descending order.
We haven’t defined the all_by_user
scope inside the Private::Conversation
model yet. Open the model and define the scope:
We haven't defined the all_by_user
scope inside the Private::Conversation
model yet. Open the model and define the scope:
Write specs for the service and the scope:
Write specs for the service and the scope:
Commit the changes.
Commit the changes.
git add -A
git commit -m "
- Create an OrderConversationsService and add specs for it
- Define an all_by_user scope inside the Private::Conversation
model and add specs for it"
Now inside views, we have an access to an array of ordered conversations. Let’s render a list of their links. Whenever a user clicks on any of them, a conversation window gets rendered on the app. If you recall, our navigation bar has two major components. Inside one component, elements are displayed constantly. Within another component, elements collapse on smaller devices. So inside the navigation’s header, where components are visible all the time, we’re going to create a drop down menu of conversations. As usually, to prevent having a large view file, split it into multiple smaller ones.
Now inside views, we have an access to an array of ordered conversations. Let's render a list of their links. Whenever a user clicks on any of them, a conversation window gets rendered on the app. If you recall, our navigation bar has two major components. Inside one component, elements are displayed constantly. Within another component, elements collapse on smaller devices. So inside the navigation's header, where components are visible all the time, we're going to create a drop down menu of conversations. As usually, to prevent having a large view file, split it into multiple smaller ones.
Open the navigation’s _header.html.erb
file and replace its content with the following:
Open the navigation's _header.html.erb
file and replace its content with the following:
Now create a header
directory with a _toggle_button.html.erb
file inside
Now create a header
directory with a _toggle_button.html.erb
file inside
This is a toggle button which was formerly located inside the _header.html.erb
file. Create another file inside the header
directory
This is a toggle button which was formerly located inside the _header.html.erb
file. Create another file inside the header
directory
And this is the home button from the _header.html.erb
. Also there is an extra link here. On smaller devices we’re going to display an icon, instead of the name of the application.
And this is the home button from the _header.html.erb
. Also there is an extra link here. On smaller devices we're going to display an icon, instead of the name of the application.
Look back at the _header.html.erb
file. There is a helper method nav_header_content_partials
, which returns an array of partials’ paths. The reason why we don’t just render partials one by one is because the array is going to differ in different cases. Inside the NavigationHelper
define the method
Look back at the _header.html.erb
file. There is a helper method nav_header_content_partials
, which returns an array of partials' paths. The reason why we don't just render partials one by one is because the array is going to differ in different cases. Inside the NavigationHelper
define the method
Write specs for the methods inside the navigation_helper_spec.rb
Write specs for the methods inside the navigation_helper_spec.rb
Now create necessary files to display drop down menus on the navigation bar. Start by creating a _dropdowns.html.erb
file
Now create necessary files to display drop down menus on the navigation bar. Start by creating a _dropdowns.html.erb
file
Create a dropdowns
directory with a _conversations.html.erb
file inside
Create a dropdowns
directory with a _conversations.html.erb
file inside
This where we use the @all_conversations
instance variable, defined inside the controller before, and render links to open conversations. Links for different type of conversations are going to differ. We’ll need to create two different versions of links for private and group conversations. First define the conversation_header_partial_path
helper method inside the NavigationHelper
This where we use the @all_conversations
instance variable, defined inside the controller before, and render links to open conversations. Links for different type of conversations are going to differ. We'll need to create two different versions of links for private and group conversations. First define the conversation_header_partial_path
helper method inside the NavigationHelper
Write specs for it:
Write specs for it:
Of course we haven’t done anything with group conversations yet. So you have to comment out the group conversation’s part in specs for a while to avoid failure.
Of course we haven't done anything with group conversations yet. So you have to comment out the group conversation's part in specs for a while to avoid failure.
Create a file for private conversations’ links:
Create a file for private conversations' links:
Define the private_conv_seen_status
helper method inside a new Shared::ConversationsHelper
Define the private_conv_seen_status
helper method inside a new Shared::ConversationsHelper
Add this this module to the Private::ConversationsHelper
Add this this module to the Private::ConversationsHelper
```include Shared::ConversationsHelper
```include Shared::ConversationsHelper
Inside specs create a shared
directory with a conversations_helper_spec.rb
file to test the private_conv_seen_status
helper method.
Inside specs create a shared
directory with a conversations_helper_spec.rb
file to test the private_conv_seen_status
helper method.
When a link to a conversation is clicked, the Private::Conversation
controller’s open
action gets called. Define a route to this action. Inside the routes.rb
file, add a post :open
member inside the namespaced privateconversations
resources, just below the post :close
.
When a link to a conversation is clicked, the Private::Conversation
controller's open
action gets called. Define a route to this action. Inside the routes.rb
file, add a post :open
member inside the namespaced privateconversations
resources, just below the post :close
.
Of course don’t forget to define the action itself inside the controller:
Of course don't forget to define the action itself inside the controller:
Now a conversation window should open when you click on its link. The navigation bar right now is messy, we have to take care of its design. To style the drop down menus, add CSS to the navigation.scss
file.
Now a conversation window should open when you click on its link. The navigation bar right now is messy, we have to take care of its design. To style the drop down menus, add CSS to the navigation.scss
file.
Update the max-width: 767px
media query inside the mobile.scss
Update the max-width: 767px
media query inside the mobile.scss
Update the min-width: 767px
media query in desktop.scss
Update the min-width: 767px
media query in desktop.scss
The app looks like this now
The app looks like this now
Then you can expand the conversations list
Then you can expand the conversations list
By clicking on any of the menu links, a conversation window should appear on the app
By clicking on any of the menu links, a conversation window should appear on the app
If you try to contract the browser’s size, conversations should be hidden one by one
If you try to contract the browser's size, conversations should be hidden one by one
Also notice that instead of the collabfield logo we have the home page icon now. And the conversations list is still available on smaller screens. Well, if conversations’ windows are hidden on smaller devices, how users are going to communicate on mobile devices? We’ll create a messenger which will be opened instead of a conversation window.
Also notice that instead of the collabfield logo we have the home page icon now. And the conversations list is still available on smaller screens. Well, if conversations' windows are hidden on smaller devices, how users are going to communicate on mobile devices? We'll create a messenger which will be opened instead of a conversation window.
Commit the changes
Commit the changes
git add -A
git commit -m "Render a drop down menu of conversations links
- split layouts/navigation/_header.html.erb file's content into partials
- Create a _toggle_button.erb.html inside layouts/navigation/header
- Create a _home_button.html.erb inside layouts/navigation/header
- Define a nav_header_content_partials inside NavigationHelper
and write specs for it
- Create a _dropdowns.html.erb inside layouts/navigation/header
- Create a _conversation.html.erb inside
layouts/navigation/header/dropdowns
- Define a conversation_header_partial_path inside NavigationHelper
and write specs for it
- Create a _private.html.erb inside
layouts/navigation/header/dropdowns/conversations
- Define a private_conv_seen_status inside
Shared::ConversationsHelper and write specs for it
- Define an open action inside the Private::Conversations controller
- add CSS to style drop down menus on the navigation bar.
Inside navigation.scss, mobile.scss and desktop.scss"
It’s a good time to make sure that all features of the real time messaging works correctly.
It's a good time to make sure that all features of the real time messaging works correctly.
Because we’re adding elements to the DOM dynamically, sometimes elements are added too late and Capybara thinks that an element doesn’t exist, because the wait time by default is 2 seconds only. To avoid these failures, inside the rails_helper.rb
, change wait time somewhere between 5 to 10 seconds.
Because we're adding elements to the DOM dynamically, sometimes elements are added too late and Capybara thinks that an element doesn't exist, because the wait time by default is 2 seconds only. To avoid these failures, inside the rails_helper.rb
, change wait time somewhere between 5 to 10 seconds.
Inside the spec/features/private/conversations folder
create a window_spec.rb
file.
Inside the spec/features/private/conversations folder
create a window_spec.rb
file.
Here I haven’t defined specs, to test if a recipient user receives messages in real time. Try to figure out how to write such tests on your own.
Here I haven't defined specs, to test if a recipient user receives messages in real time. Try to figure out how to write such tests on your own.
Commit the changes
Commit the changes
git add -A
git commit -m "Add specs to test the conversation window's functionality"
If you logged in an account which has received messages, you would notice a conversation, marked as an unseen
If you logged in an account which has received messages, you would notice a conversation, marked as an unseen
At this moment there is no way to mark conversations as seen. By default a new message has an unseen value. Program the app in a way that when a conversation window is opened or clicked, its messages get marked as seen. Also note that currently we only see highlighted unseen conversations when the drop down menu is expanded. In the future we will create a notifications feature, so users will know that they got new messages without expanding anything.
At this moment there is no way to mark conversations as seen. By default a new message has an unseen value. Program the app in a way that when a conversation window is opened or clicked, its messages get marked as seen. Also note that currently we only see highlighted unseen conversations when the drop down menu is expanded. In the future we will create a notifications feature, so users will know that they got new messages without expanding anything.
Let’s tackle the first problem. When a conversation is already rendered on the app, but it is collapsed, and a user clicks on the drop down menu’s link to open that conversation, nothing happens. That collapsed conversation stays collapsed. We’ve to add some JavaScript, so in the case of the drop down menu’s link click, the conversation should expand and focus its new message area.
Let's tackle the first problem. When a conversation is already rendered on the app, but it is collapsed, and a user clicks on the drop down menu's link to open that conversation, nothing happens. That collapsed conversation stays collapsed. We've to add some JavaScript, so in the case of the drop down menu's link click, the conversation should expand and focus its new message area.
Open the file below and add the code from the Gist to achieve that.
Open the file below and add the code from the Gist to achieve that.
assets/javascripts/conversations/toggle_window.js
When you click on a link, to open a conversation window, no matter if a conversation is already present on the app, or not, it will be expanded.
When you click on a link, to open a conversation window, no matter if a conversation is already present on the app, or not, it will be expanded.
Now we need an event handler. After a conversation window which has unseen messages is clicked, the private conversation client’s side should fire a callback function. First, define an event handler inside the private conversation client’s side, at the bottom of the file
Now we need an event handler. After a conversation window which has unseen messages is clicked, the private conversation client's side should fire a callback function. First, define an event handler inside the private conversation client's side, at the bottom of the file
A case of messenger’s existence is already included in this snippet of code.
A case of messenger's existence is already included in this snippet of code.
Then, define the callback function inside the private_conversation
instance, just below the send_message
function
Then, define the callback function inside the private_conversation
instance, just below the send_message
function
Finally, define this method on the server side
Finally, define this method on the server side
After a user clicks on a link to open a conversation window or clicks directly on a conversation window, its unseen messages will be marked as seen.
After a user clicks on a link to open a conversation window or clicks directly on a conversation window, its unseen messages will be marked as seen.
Commit the changes
Commit the changes
git add -A
git commit -m "Add ability to mark unseen messages as seen
- Add an event handler to expand conversation windows inside the
assets/javascripts/conversations/toggle_window.js
- Add an event handler to mark unseen messages as seen inside the
assets/javascripts/channels/private/conversation.js
- Define a set_as_seen method for Private::ConversationChannel"
Make sure that everything works as we expect by writing the specs.
Make sure that everything works as we expect by writing the specs.
联络人 (Contacts)
To stay in touch with people, you met on the app, you have to be able to add them to contacts. We’re missing this functionality right now. Also having a contacts feature opens a lot of possibilities to create other features that only users who are accepted as a contact could perform.
To stay in touch with people, you met on the app, you have to be able to add them to contacts. We're missing this functionality right now. Also having a contacts feature opens a lot of possibilities to create other features that only users who are accepted as a contact could perform.
Generate a Contact
model
Generate a Contact
model
rails g model contact
Define associations, validation and a method to find a contact record by providing users’ ids.
Define associations, validation and a method to find a contact record by providing users' ids.
Define the contacts table
Define the contacts table
A factory for contacts is going to be needed. Define it:
A factory for contacts is going to be needed. Define it:
Write specs to test the model
Write specs to test the model
Commit the changes
Commit the changes
git add -A
git commit -m "Create a Contact model and write specs for it"
Inside the User
model’s file, we have to define appropriate associations and also define some methods to help with contacts’ queries.
Inside the User
model's file, we have to define appropriate associations and also define some methods to help with contacts' queries.
Cover associations and methods with specs
Cover associations and methods with specs
Commit the changes
Commit the changes
git add -A
git commit -m "Add associations and helper methods to the User model
- Create relationship between the User the Contact models
- Methods help query the Contact records"
Generate a Contacts
controller and define its actions
Generate a Contacts
controller and define its actions
rails g controller contacts
As you see, users will be able to create a new contact record, update its status (accept a user to their contacts) and remove a user from their contact list. Because all actions are called via AJAX and we don’t want to render any templates as a response, we respond with a success response. This way Rails doesn’t have to think what to respond with.
As you see, users will be able to create a new contact record, update its status (accept a user to their contacts) and remove a user from their contact list. Because all actions are called via AJAX and we don't want to render any templates as a response, we respond with a success response. This way Rails doesn't have to think what to respond with.
Define the corresponding routes:
Define the corresponding routes:
Commit the changes.
Commit the changes.
git add -A
git commit -m "Create a ContactsController and define routes to its actions"
Private conversation's window update (Private conversation’s window update)
The way users are going to be able to send and accept contact requests is through a private conversation’s window. Later we’ll add an extra way to accept requests through a navigation bar’s drop down menu.
The way users are going to be able to send and accept contact requests is through a private conversation's window. Later we'll add an extra way to accept requests through a navigation bar's drop down menu.
Create a new heading
directory
Create a new heading
directory
private/conversations/conversation/heading
This is where we’ll keep extra options for a private conversation’s window. Inside the directory, create a _add_user_to_contacts.html.erb
file
This is where we'll keep extra options for a private conversation's window. Inside the directory, create a _add_user_to_contacts.html.erb
file
At the bottom of the _heading.html.erb
file, render the option to add the conversation’s opposite user to contacts:
At the bottom of the _heading.html.erb
file, render the option to add the conversation's opposite user to contacts:
Define the helper method and additional methods within a private scope
Define the helper method and additional methods within a private scope
Write specs for these helper methods
Write specs for these helper methods
The instance_eval
method is used to test methods within a private scope.
The instance_eval
method is used to test methods within a private scope.
Because we’re going to display options on the conversation window’s heading element, we have to make sure that additional options fit perfectly on the heading. Inside the _heading.html.erb
file, replace the conversation-heading
class with <%= conv_heading_class(@contact) %>
, to determine which class to add.
Because we're going to display options on the conversation window's heading element, we have to make sure that additional options fit perfectly on the heading. Inside the _heading.html.erb
file, replace the conversation-heading
class with <%= conv_heading_class(@contact) %>
, to determine which class to add.
Define the helper method
Define the helper method
Write specs for the method
Write specs for the method
The options, to send or accept a contact request, won’t be shown yet. More elements need to be added. Open the _conversation.html.erb
file
The options, to send or accept a contact request, won't be shown yet. More elements need to be added. Open the _conversation.html.erb
file
private/conversations/_conversation.html.erb
At the top of the file, define a @contact
instance variable, so it is accessible across all partials
At the top of the file, define a @contact
instance variable, so it is accessible across all partials
Define the get_contact_record
helper method
Define the get_contact_record
helper method
Cover the method with specs
Cover the method with specs
Previously, we used current_user
and recipient
let
methods only within a private scope’s context. Now we need access to them on both private and public methods. So cut and place them outside of private scope’s context.
Previously, we used current_user
and recipient
let
methods only within a private scope's context. Now we need access to them on both private and public methods. So cut and place them outside of private scope's context.
At the top of the .panel-body
element, render a partial file which will show an extra message window to accept or decline a contact request
At the top of the .panel-body
element, render a partial file which will show an extra message window to accept or decline a contact request
Create the _request_status.html.erb
file
Create the _request_status.html.erb
file
Define the needed helper methods
Define the needed helper methods
Writes specs for the helper methods
Writes specs for the helper methods
Create the request_status
directory and then create _send_request.html.erb
, _sent_by_current_user.html.erb
and _sent_by_recipient.html.erb
partial files
Create the request_status
directory and then create _send_request.html.erb
, _sent_by_current_user.html.erb
and _sent_by_recipient.html.erb
partial files
Commit the changes
Commit the changes
git add -A
git commit -m "Add a button on private conversation's window
to add a recipient to contacts"
Implement design changes and take care of styling issues which appear due to extra elements on the conversation window. Add CSS to the conversation_window.scss
file
Implement design changes and take care of styling issues which appear due to extra elements on the conversation window. Add CSS to the conversation_window.scss
file
Commit the changes
Commit the changes
git add -A
git commit -m "Add CSS to conversation_window.scss to style option buttons"
When a conversation window is collapsed, it would be better to not see any options. It’s a more convenient design to see options only when a conversation window is expanded. To achieve it, inside the toggle_window.js
file’s toggle function, just below the messages_visible
variable, add
When a conversation window is collapsed, it would be better to not see any options. It's a more convenient design to see options only when a conversation window is expanded. To achieve it, inside the toggle_window.js
file's toggle function, just below the messages_visible
variable, add
Now the collapsed window looks like this, it has no visible options
Now the collapsed window looks like this, it has no visible options
The expanded window has an option to add a user to contacts. Also there’s a message which suggests to do that
The expanded window has an option to add a user to contacts. Also there's a message which suggests to do that
Actually, you can send and accept a contact request right now by clicking an icon on the conversation’s header or clicking the Add to contacts
link. For now, there isn’t any response after you click on those links and buttons. We’ll add some feedback and real time notification system a little bit later. But technically, you can add users to your contacts, it is just not highly user friendly yet.
Actually, you can send and accept a contact request right now by clicking an icon on the conversation's header or clicking the Add to contacts
link. For now, there isn't any response after you click on those links and buttons. We'll add some feedback and real time notification system a little bit later. But technically, you can add users to your contacts, it is just not highly user friendly yet.
After you send a contact request, the opposite user’s side looks like this
After you send a contact request, the opposite user's side looks like this
Commit the changes
Commit the changes
git add -A
git commit -m "Add JS inside the toggle_window.js to show and hide additional options"
Currently users are able to talk privately, have one on one conversations. Since the app is about collaboration, it would be logical to have group conversations too. (Currently users are able to talk privately, have one on one conversations. Since the app is about collaboration, it would be logical to have group conversations too.)
Start by generating a new model
Start by generating a new model
rails g model group/conversation
Multiple users will be able to participate in one conversation. Define associations and the database table
Multiple users will be able to participate in one conversation. Define associations and the database table
A join table is going to be used to track who belongs to which group conversation
A join table is going to be used to track who belongs to which group conversation
Then generate a model for messages
Then generate a model for messages
rails g model group/message
We’ll store users’ ids who have seen a message into an array. To create and manage objects, such as array, inside a database column, a serialize method is used. A default scope, to minimize the amount of queries, and some validations are added.
We'll store users' ids who have seen a message into an array. To create and manage objects, such as array, inside a database column, a serialize method is used. A default scope, to minimize the amount of queries, and some validations are added.
The way we’re building group conversations is pretty similar to private conversations. In fact, styling and some parts are going to be in common between both types of conversations.
The way we're building group conversations is pretty similar to private conversations. In fact, styling and some parts are going to be in common between both types of conversations.
Write specs for the models. Also a factory for group messages is going to be needed
Write specs for the models. Also a factory for group messages is going to be needed
require 'rails_helper'
RSpec.describe Group::Message, type: :model do
let(:message) { build(:group_message) }
context 'Associations' do
it 'belongs_to group_conversation' do
association = described_class.reflect_on_association(:conversation)
expect(association.macro).to eq :belongs_to
expect(association.options[:class_name]).to eq 'Group::Conversation'
expect(association.options[:foreign_key]).to eq 'conversation_id'
end
end
context 'Validations' do
it "is not valid without a content" do
message.content = nil
expect(message).not_to be_valid
end
it "is not valid without a conversation_id" do
message.conversation_id = nil
expect(message).not_to be_valid
end
it "is not valid without a user_id" do
message.user_id = nil
expect(message).not_to be_valid
end
end
context 'Methods' do
it 'gets a previous message of a conversation' do
conversation = create(:group_conversation)
message1 = create(:group_message, conversation_id: conversation.id)
message2 = create(:group_message, conversation_id: conversation.id)
expect(message2.previous_message).to eq message1
end
end
end
Define the migration files
Define the migration files
The fundamentals of the group conversation are set.
The fundamentals of the group conversation are set.
Commit the changes
Commit the changes
git add -A
git commit -m "Create Group::Conversation and Group::Message models
- Define associations
- Write specs"
Create a group conversation
Create a group conversation
As mentioned before, the process of creating the group conversation feature is going to be similar to what we did with the private conversation. Start by creating a controller and a basic user interface.
As mentioned before, the process of creating the group conversation feature is going to be similar to what we did with the private conversation. Start by creating a controller and a basic user interface.
Generate a namespaced controller
Generate a namespaced controller
rails g controller group/conversations
Inside the controller define a create
action and add_to_conversations
, already_added?
and create_group_conversation
methods within a private scope
Inside the controller define a create
action and add_to_conversations
, already_added?
and create_group_conversation
methods within a private scope
There is some complexity involved in creating a new group conversation, so we’ll extract it into a service object. Then we have add_to_conversations
and already_added?
private methods. If you recall, we have them in the Private::ConversationsController
too, but this time it stores group conversations’ ids into the session.
There is some complexity involved in creating a new group conversation, so we'll extract it into a service object. Then we have add_to_conversations
and already_added?
private methods. If you recall, we have them in the Private::ConversationsController
too, but this time it stores group conversations' ids into the session.
Now define the Group::NewConversationService
inside a new group
directory
Now define the Group::NewConversationService
inside a new group
directory
The way a new group conversation is going to be created, is actually through a private conversation. We’ll create this interface as an option on the private conversation’s window soon. Before doing that, make sure that the service object functions properly by covering it with specs. Inside the services
, create a new directory group
with a new_conversation_service_spec.rb
file inside
The way a new group conversation is going to be created, is actually through a private conversation. We'll create this interface as an option on the private conversation's window soon. Before doing that, make sure that the service object functions properly by covering it with specs. Inside the services
, create a new directory group
with a new_conversation_service_spec.rb
file inside
Commit the changes
Commit the changes
git add -A
git commit -m "Create back-end for creating a new group conversation
- Create a Group::ConversationsController
Define a create action and add_to_conversations,
create_group_conversation and already_added? private methods inside
- Create a Group::NewConversationService and write specs for it"
Define routes for the group conversation and its messages
Define routes for the group conversation and its messages
Commit the changes
Commit the changes
git add -A
git commit -m "Define specs for Group::Conversations and Messages"
Currently we only take care of private conversations inside the ApplicationController
. Only private conversations are being ordered and only their ids, after a user opens them, are available across the app. Inside the ApplicationController
, update the opened_conversations_windows
method
Currently we only take care of private conversations inside the ApplicationController
. Only private conversations are being ordered and only their ids, after a user opens them, are available across the app. Inside the ApplicationController
, update the opened_conversations_windows
method
Because conversations’ ordering happens with a help of the OrderConversationsService
, we’ve to update this service
Because conversations' ordering happens with a help of the OrderConversationsService
, we've to update this service
Previously we only had the private conversations array and we sorted it by the latest messages’ creation dates. Now we have private and group conversations arrays, then we join them together into one array and sort it the same way, as we did before.
Previously we only had the private conversations array and we sorted it by the latest messages' creation dates. Now we have private and group conversations arrays, then we join them together into one array and sort it the same way, as we did before.
Also update the specs
Also update the specs
Commit the changes
Commit the changes
git add -A
git commit -m "Get data for group conversations in ApplicationController
- Update the opened_conversations_windows method
- Update the OrderConversationsService"
In just a moment we’ll need to pass some data from a controller to JavaScript. Luckily, we have already installed the gon
gem, which allows us to do that easily. Inside the ApplicationController
, within a private
scope, add
In just a moment we'll need to pass some data from a controller to JavaScript. Luckily, we have already installed the gon
gem, which allows us to do that easily. Inside the ApplicationController
, within a private
scope, add
Use the before_action
filter to call this method
Use the before_action
filter to call this method
before_action :set_user_data
Commit the changes
Commit the changes
git add -A
git commit -m "Define a set_user_data private method in ApplicationController"
Technically we can create a new group conversation now, but users have no interface to do that. As mentioned, they will do it through a private conversation. Let’s create this option on the private conversation’s window.
Technically we can create a new group conversation now, but users have no interface to do that. As mentioned, they will do it through a private conversation. Let's create this option on the private conversation's window.
Inside the
Inside the
views/private/conversations/conversation/heading
directory create a new file
directory create a new file
A collection_select
method is used to display a list of users. Only users who are in contacts are going to be included in the list. Define the contacts_except_recipient
helper method
A collection_select
method is used to display a list of users. Only users who are in contacts are going to be included in the list. Define the contacts_except_recipient
helper method
Write specs for the method
Write specs for the method
Render the partial at the bottom of the _heading.html.erb
Render the partial at the bottom of the _heading.html.erb
Define the helper method
Define the helper method
Wrap it with specs
Wrap it with specs
Commit the changes.
Commit the changes.
git add -A
git commit -m "Add a UI on private conversation to create a group conversations"
Add CSS to style the component which allows to create a new group conversation
Add CSS to style the component which allows to create a new group conversation
A selection of contacts is hidden by default. To open the selection, a user has to click on the button. The button isn’t interactive yet. Create an options.js
file with JavaScript inside to make the selection list toggleable.
A selection of contacts is hidden by default. To open the selection, a user has to click on the button. The button isn't interactive yet. Create an options.js
file with JavaScript inside to make the selection list toggleable.
Now a conversation window with a recipient who is a contact looks like this
Now a conversation window with a recipient who is a contact looks like this
There is a button which opens a list of contacts, you can create a group conversation with, when you click on it
There is a button which opens a list of contacts, you can create a group conversation with, when you click on it
Commit the changes.
Commit the changes.
git add -A
git commit -m "
- Describe style for the create a group conversation option
- Make the option toggleable"
We have a list of ordered conversations, including group conversations now, which will be rendered on the navigation bar’s drop down menu. If you recall, we specified different partials for different types of conversations. When the app tries to render a link, to open a group conversation, it will look for a different file than for a private conversation. The file isn’t created yet.
We have a list of ordered conversations, including group conversations now, which will be rendered on the navigation bar's drop down menu. If you recall, we specified different partials for different types of conversations. When the app tries to render a link, to open a group conversation, it will look for a different file than for a private conversation. The file isn't created yet.
Create a _group.html.erb
file
Create a _group.html.erb
file
Define the group_conv_seen_status
helper method inside the Shared::ConversationsHelper
Define the group_conv_seen_status
helper method inside the Shared::ConversationsHelper
Write specs for the method
Write specs for the method
Commit the changes.
Commit the changes.
git add -A
git commit -m "Create a link on the navigation bar to open a group conversation"
Render group conversations’ windows on the app, the same way as we rendered the private conversations. Inside the application.html.erb
, just below the rendered private conversations, add:
Render group conversations' windows on the app, the same way as we rendered the private conversations. Inside the application.html.erb
, just below the rendered private conversations, add:
Create the partial file to render group conversations’ windows one by one:
Create the partial file to render group conversations' windows one by one:
Commit the change.
Commit the change.
git add -A
git commit -m "Render group conversations' windows inside the
application.html.erb"
We have a mechanism how group conversations are created and rendered on the app. Now let’s build a conversation window itself.
We have a mechanism how group conversations are created and rendered on the app. Now let's build a conversation window itself.
Inside the group/conversations
directory, create a _conversation.html.erb
file.
Inside the group/conversations
directory, create a _conversation.html.erb
file.
Define the add_people_to_group_conv_list
helper method:
Define the add_people_to_group_conv_list
helper method:
Write specs for the helper:
Write specs for the helper:
Just like with private conversations, group conversations are going to be accessible across the whole app, so obviously, we need access to the Group::ConversationsHelper
methods everywhere too. Add this module inside the ApplicationHelper
Just like with private conversations, group conversations are going to be accessible across the whole app, so obviously, we need access to the Group::ConversationsHelper
methods everywhere too. Add this module inside the ApplicationHelper
include Group::ConversationsHelper
Commit the changes.
Commit the changes.
git add -A
git commit -m "
- Create a _conversation.html.erb inside the group/conversations
- Define a add_people_to_group_conv_list and write specs for it"
Create a new conversation
directory with a _heading.html.erb
file inside:
Create a new conversation
directory with a _heading.html.erb
file inside:
Commit the change.
Commit the change.
git add -A
git commit -m "Create a _heading.html.erb inside the
group/conversations/conversation"
Next we have _select_user.html.erb
and _messages_list.html.erb
partial files. Create them:
Next we have _select_user.html.erb
and _messages_list.html.erb
partial files. Create them:
Define the load_group_messages_partial_path
helper method:
Define the load_group_messages_partial_path
helper method:
Wrap it with specs:
Wrap it with specs:
Commit the changes.
Commit the changes.
git add -A
git commit -m "
- Create _select_user.html.erb and _messages_list.html.erb inside
group/conversations/conversation
- Define a load_group_messages_partial_path helper method
and write specs for it"
Create a _link_to_previous_messages.html.erb
file, to have a link which loads previous messages:
Create a _link_to_previous_messages.html.erb
file, to have a link which loads previous messages:
Commit the change.
Commit the change.
git add -A
git commit -m "Create a _load_messages.html.erb inside the
group/conversations/conversation/messages_list"
Create a new message form
Create a new message form
Commit the change.
Commit the change.
git add -A
git commit -m "Create a _new_message_form.html.erb inside the
group/conversations/conversation/"
The application is now able to render group conversations’ windows too.
The application is now able to render group conversations' windows too.
But, they aren’t functional yet. First, we need to load messages. We need a controller for messages and views. Generate a Messages
controller:
But, they aren't functional yet. First, we need to load messages. We need a controller for messages and views. Generate a Messages
controller:
rails g controller group/messages
Include the Messages
module from concerns
and define an index
action:
Include the Messages
module from concerns
and define an index
action:
Commit the changes.
Commit the changes.
git add -A
git commit -m "Create a Group::MessagesController and define an index action"
Create a _load_more_messages.js.erb
Create a _load_more_messages.js.erb
We’ve already defined the append_previous_messages_partial_path
and remove_link_to_messages
helper methods earlier on. We only have to define the replace_link_to_group_messages_partial_path
helper method
We've already defined the append_previous_messages_partial_path
and remove_link_to_messages
helper methods earlier on. We only have to define the replace_link_to_group_messages_partial_path
helper method
Again, this method, just like on the private side, is going to become more “intelligent”, once we develop the messenger.
Again, this method, just like on the private side, is going to become more “intelligent”, once we develop the messenger.
Create the _replace_link_to_messages.js.erb
Create the _replace_link_to_messages.js.erb
Also add the Group::MessagesHelper
to the ApplicationHelper
Also add the Group::MessagesHelper
to the ApplicationHelper
include Group::MessagesHelper
Commit the changes.
Commit the changes.
git add -A
git commit -m "Create a _load_more_messages.js.erb inside the group/messages"
The only way group conversations can be opened right now is after their initialization. Obviously, this is not a thrilling thing, because once you destroy the session, there is no way to open the same conversation again. Create an open
action inside the controller.
The only way group conversations can be opened right now is after their initialization. Obviously, this is not a thrilling thing, because once you destroy the session, there is no way to open the same conversation again. Create an open
action inside the controller.
Create the _open.js.erb
partial file:
Create the _open.js.erb
partial file:
Now we’re able to open conversations by clicking on navigation bar’s drop down menu links. Try to test it with feature specs on your own.
Now we're able to open conversations by clicking on navigation bar's drop down menu links. Try to test it with feature specs on your own.
Commit the changes.
Commit the changes.
git add -A
git commit -m "Add ability to open group conversations
- Create an open action in the Group::ConversationsController
- Create an _open.js.erb inside the group/conversations"
The app will try to render messages, but we haven’t created any templates for them. Create a _message.html.erb
The app will try to render messages, but we haven't created any templates for them. Create a _message.html.erb
Define group_message_date_check_partial_path
, group_message_seen_by
and message_content_partial_path
helper methods.
Define group_message_date_check_partial_path
, group_message_seen_by
and message_content_partial_path
helper methods.
The group_message_seen_by
method will return a list of users who have seen a message. This little information allows us to create extra features, like show to conversation participants who have seen messages, etc. But in our case, we’ll use this information to determine if a current user has seen a message, or not. If not, then after the user sees it, the message is going to be marked as seen.
The group_message_seen_by
method will return a list of users who have seen a message. This little information allows us to create extra features, like show to conversation participants who have seen messages, etc. But in our case, we'll use this information to determine if a current user has seen a message, or not. If not, then after the user sees it, the message is going to be marked as seen.
Also we’ll need helper methods from the Shared
module. Inside the Group::MessagesHelper
, add the module.
Also we'll need helper methods from the Shared
module. Inside the Group::MessagesHelper
, add the module.
require 'shared/messages_helper'
include Shared::MessagesHelper
Wrap helper methods with specs:
Wrap helper methods with specs:
Commit the changes.
Commit the changes.
git add -A
git commit -m "Create a _message.html.erb inside group/messages
- Define group_message_date_check_partial_path,
group_message_seen_by and message_content_partial_path helper
methods and write specs for them"
Create partial files for the message:
Create partial files for the message:
Commit the changes.
Commit the changes.
git add -A
git commit -m "Create _new_date.html.erb,
_different_user_content.html.erb and _same_user_content.html.erb
inside the group/messages/message/"
Now we need a mechanism which will render messages one by one. Create a _messages.html.erb
partial file:
Now we need a mechanism which will render messages one by one. Create a _messages.html.erb
partial file:
Commit the change.
Commit the change.
git add -A
git commit -m "Create _messages.html.erb inside group/conversations"
Add styling for group messages:
Add styling for group messages:
Commit the change.
Commit the change.
git add -A
git commit -m "Add CSS for group messages in conversation_window.scss"
Make the close button functional by defining a close
action inside the Group::ConversationsController
Make the close button functional by defining a close
action inside the Group::ConversationsController
Create the corresponding template file:
Create the corresponding template file:
Commit the changes.
Commit the changes.
git add -A
git commit -m "Add a close group conversation window functionality
- Define a close action inside the Group::ConversationsController
- Create a close.js.erb inside the group/conversations"
Communicating in real time
Communicating in real time
Just like with private conversations, we want to be able to have conversations in real time with multiple users at the same window. The process of achieving this feature is going to be pretty similar to what we did with private conversations.
Just like with private conversations, we want to be able to have conversations in real time with multiple users at the same window. The process of achieving this feature is going to be pretty similar to what we did with private conversations.
Generate a new channel for group conversations
Generate a new channel for group conversations
rails g channel group/conversation
This time we check if a user belongs to a conversation, before establishing the connection, with the belongs_to_conversation
method. In private conversations we streamed from a unique channel, by providing the current_user
’s id. In a case of group conversations, an id of a conversation is passed from the client side. With the belongs_to_conversation
method we check if users didn’t do any manipulations and didn’t try to connect to a channel which they don’t belong to.
This time we check if a user belongs to a conversation, before establishing the connection, with the belongs_to_conversation
method. In private conversations we streamed from a unique channel, by providing the current_user
's id. In a case of group conversations, an id of a conversation is passed from the client side. With the belongs_to_conversation
method we check if users didn't do any manipulations and didn't try to connect to a channel which they don't belong to.
Commit the change
Commit the change
git add -A
git commit -m "Create a Group::ConversationChannel"
Create the Group::MessageBroadcastJob
Create the Group::MessageBroadcastJob
rails g job group/message_broadcast
Commit the change.
Commit the change.
git add -A
git commit -m "Create a Group::MessageBrodcastJob"
The last missing puzzle piece left — the client side:
The last missing puzzle piece left — the client side:
Essentially, it’s very similar to the private conversation’s .js
file. The layout of the code is a little bit different. The main difference is an ability to pass conversation’s id
to a channel and a loop at the top of the file. With this loop we connect a user to all its group conversations’ channels. That is the reason why we have used the belongs_to_conversation
method on the server side. Id’s of the conversations are passed from the client side. This method on the server side makes sure that a user really belongs to a provided conversation.
Essentially, it's very similar to the private conversation's .js
file. The layout of the code is a little bit different. The main difference is an ability to pass conversation's id
to a channel and a loop at the top of the file. With this loop we connect a user to all its group conversations' channels. That is the reason why we have used the belongs_to_conversation
method on the server side. Id's of the conversations are passed from the client side. This method on the server side makes sure that a user really belongs to a provided conversation.
When you think about it, we could have just created this loop on the server side and wouldn’t have to deal with all this confirmation process. But here’s a reason why we pass an id of a conversation from the client side. When new users get added to a group conversation, we want to connect them immediately to the conversation’s channel, without requiring to reload a page. The passable conversation’s id allows us to effortlessly achieve that. In the upcoming section we’ll create a unique channel for every user to receive notifications in real time. When new users will be added to a group conversation, we’ll call the subToGroupConversationChannel
function, through their unique notification channels, and connect them to the group conversation channel. If we didn’t allow to pass a conversation’s id to a channel, connections to new channels would have occurred only after a page reload. We wouldn’t have any way to connect new users to a conversation channel dynamically.
When you think about it, we could have just created this loop on the server side and wouldn't have to deal with all this confirmation process. But here's a reason why we pass an id of a conversation from the client side. When new users get added to a group conversation, we want to connect them immediately to the conversation's channel, without requiring to reload a page. The passable conversation's id allows us to effortlessly achieve that. In the upcoming section we'll create a unique channel for every user to receive notifications in real time. When new users will be added to a group conversation, we'll call the subToGroupConversationChannel
function, through their unique notification channels, and connect them to the group conversation channel. If we didn't allow to pass a conversation's id to a channel, connections to new channels would have occurred only after a page reload. We wouldn't have any way to connect new users to a conversation channel dynamically.
Now we are able to send and receive group messages in real time. Try to test the overall functionality with specs on your own.
Now we are able to send and receive group messages in real time. Try to test the overall functionality with specs on your own.
Commit the changes.
Commit the changes.
git add -A
git commit -m "Create a conversation.js inside the
assets/javascripts/channels/group"
Inside the Group::ConversationsController
define an update
action
Inside the Group::ConversationsController
define an update
action
Create the Group::AddUserToConversationService
, which is going to take care that a selected user will be added to a conversation
Create the Group::AddUserToConversationService
, which is going to take care that a selected user will be added to a conversation
Test the service with specs:
Test the service with specs:
We have working private and group conversations now. A few nuances are still missing, which we will implement later, but the core functionality is here. Users are able to communicate one on one, or if they need, they can build an entire chat room with multiple people.
We have working private and group conversations now. A few nuances are still missing, which we will implement later, but the core functionality is here. Users are able to communicate one on one, or if they need, they can build an entire chat room with multiple people.
Commit the changes.
Commit the changes.
git add -A
git commit -m "Create a Group::AddUserToConversationService and test it"
信使 (Messenger)
What is the purpose of having a messenger? On mobile screens instead of opening a conversation window, the app will load the messenger. On bigger screens, users could choose where to chat, on the conversation window or on the messenger. If the messenger is going to fill the whole browser’s window, it should be more comfortable to communicate.
What is the purpose of having a messenger? On mobile screens instead of opening a conversation window, the app will load the messenger. On bigger screens, users could choose where to chat, on the conversation window or on the messenger. If the messenger is going to fill the whole browser's window, it should be more comfortable to communicate.
Since we’ll use the same data and models, we just need to open conversations in a different environment. Generate a new controller to handle requests to open a conversation inside the messenger.
Since we'll use the same data and models, we just need to open conversations in a different environment. Generate a new controller to handle requests to open a conversation inside the messenger.
rails g controller messengers
get_private_conversation
and get_group_conversation
actions will get a user’s selected conversation. Those actions’ templates are going to append a selected conversation to the conversation placeholder. Every time a new conversation is selected to be opened the old one gets removed and replaced with a newly selected one.
get_private_conversation
and get_group_conversation
actions will get a user's selected conversation. Those actions' templates are going to append a selected conversation to the conversation placeholder. Every time a new conversation is selected to be opened the old one gets removed and replaced with a newly selected one.
Define routes for the actions:
Define routes for the actions:
Commit the changes.
Commit the changes.
In the controller is an open_messenger action. The purpose of this action is to go from any page straight to the messenger and render a selected conversation. On smaller screens users are going to chat through messenger instead of conversation windows. In just a moment, we’ll switch links for smaller screens to open conversations inside the messenger.
Create a template for the open_messenger action
In the controller is an open_messenger
action. The purpose of this action is to go from any page straight to the messenger and render a selected conversation. On smaller screens users are going to chat through messenger instead of conversation windows. In just a moment, we’ll switch links for smaller screens to open conversations inside the messenger.
In the controller is an open_messenger
action. The purpose of this action is to go from any page straight to the messenger and render a selected conversation. On smaller screens users are going to chat through messenger instead of conversation windows. In just a moment, we'll switch links for smaller screens to open conversations inside the messenger.
Create a template for the open_messenger
action
Create a template for the open_messenger
action
git add -A
git commit -m "Create an open_messenger.html.erb in the /messengers"
Then we see the ConversationForMessengerSerivce
, it retrieves a selected conversation’s object. Create the service:
Then we see the ConversationForMessengerSerivce
, it retrieves a selected conversation's object. Create the service:
Add specs for the service:
Add specs for the service:
Commit the changes.
Commit the changes.
git add -A
git commit -m "Create a ConversationForMessengerSerivce and add specs for it"
Create a template for the index action:
Create a template for the index action:
This is going to be the messenger itself. Inside the messenger, we’ll be able to see a list of user’s conversations and a selected conversation. Create the partial files:
This is going to be the messenger itself. Inside the messenger, we'll be able to see a list of user's conversations and a selected conversation. Create the partial files:
Define the helper method:
Define the helper method:
Try to test it with specs yourself.
Try to test it with specs yourself.
Create the partial files for links to open conversations:
Create the partial files for links to open conversations:
Now create a partial for the conversation space, selected conversations are going to be rendered there:
Now create a partial for the conversation space, selected conversations are going to be rendered there:
Commit the changes.
Commit the changes.
git add -A
git commit -m "Create a template for the MessengersController's index action"
Create a template for the get_private_conversation
action:
Create a template for the get_private_conversation
action:
Create a _private_conversation.html.erb
file:
Create a _private_conversation.html.erb
file:
This file will render a private conversation inside the messenger. Also notice that we reuse some partials from the private conversation views. Create the _details.html.erb
partial:
This file will render a private conversation inside the messenger. Also notice that we reuse some partials from the private conversation views. Create the _details.html.erb
partial:
Commit the changes.
Commit the changes.
git add -A
git commit -m "Create a template for the MessengersController's
get_private_conversation action"
When we go to the messenger, it’s better to not see drop down menus on the navigation bar. Why? We don’t want to render conversation windows inside the messenger, otherwise it would look chaotic. A conversation window and the messenger at the same time to chat with the same person. That would be a highly faulty design.
When we go to the messenger, it's better to not see drop down menus on the navigation bar. 为什么? We don't want to render conversation windows inside the messenger, otherwise it would look chaotic. A conversation window and the messenger at the same time to chat with the same person. That would be a highly faulty design.
At first, forbid conversations’ windows to be rendered on the messenger’s page. Not that hard to do. To control it, remember how conversations’ windows are rendered on the app. They are rendered inside the application.html.erb
file. Then we have @private_conversations_windows
and @group_conversations_windows
instance variables. Those variables are arrays of conversations. Instead of just rendering conversations from those arrays, define helper methods to decide to give those arrays to users or not, depending on which page they are in. If users are inside the messenger’s page, they will get an empty array and no conversations’ windows will be rendered.
At first, forbid conversations' windows to be rendered on the messenger's page. Not that hard to do. To control it, remember how conversations' windows are rendered on the app. They are rendered inside the application.html.erb
file. Then we have @private_conversations_windows
and @group_conversations_windows
instance variables. Those variables are arrays of conversations. Instead of just rendering conversations from those arrays, define helper methods to decide to give those arrays to users or not, depending on which page they are in. If users are inside the messenger's page, they will get an empty array and no conversations' windows will be rendered.
Replace those instance variables with private_conversations_windows
and group_conversations_windows
helper methods. Now define them inside the ApplicationHelper
Replace those instance variables with private_conversations_windows
and group_conversations_windows
helper methods. Now define them inside the ApplicationHelper
Wrap them with specs
Wrap them with specs
Commit the changes
Commit the changes
git add -A
git commit -m "
Define private_conversations_windows and group_conversations_windows
helper methods inside the ApplicationHelper and test them"
Next, create an alternative partial file for navigation’s header, so drop down menus won’t be rendered. Inside the NavigationHelper
, we’ve defined the nav_header_content_partials
helper method before. It determines which navigation’s header to render.
Next, create an alternative partial file for navigation's header, so drop down menus won't be rendered. Inside the NavigationHelper
, we've defined the nav_header_content_partials
helper method before. It determines which navigation's header to render.
Inside the
Inside the
layouts/navigation/header
directory, create a _messenger_header.html.erb
file
directory, create a _messenger_header.html.erb
file
Style the messenger. Create a messenger.scss
file inside the partials
directory
Style the messenger. Create a messenger.scss
file inside the partials
directory
Commit the change
Commit the change
git add -A
git commit -m "Create a messenger.scss inside the partials"
Inside the desktop.scss
, within the min-width: 767px
, add
Inside the desktop.scss
, within the min-width: 767px
, add
When we click on a conversation to open it, we want to be able to load previous messages somehow. We could add a visible link for loading them. Or we can automatically load some amount of messages until the scroll bar appears, so a user could load previous messages by scrolling up. Create a helper method which will take care of it
When we click on a conversation to open it, we want to be able to load previous messages somehow. We could add a visible link for loading them. Or we can automatically load some amount of messages until the scroll bar appears, so a user could load previous messages by scrolling up. Create a helper method which will take care of it
Test it with specs on your own. Create the partial files
Test it with specs on your own. Create the partial files
Commit the changes
Commit the changes
git add -A
git commit -m "Define an autoload_messenger_messages in the
Shared::MessagesHelper"
Use the helper method inside the _load_more_messages.js.erb
file, just above the <%= render remove_link_to_messages %>
Use the helper method inside the _load_more_messages.js.erb
file, just above the <%= render remove_link_to_messages %>
Now we have append_previous_messages_partial_path
and replace_link_to_private_messages_partial_path
helper methods which we should update, to make them compatible with the messenger
Now we have append_previous_messages_partial_path
and replace_link_to_private_messages_partial_path
helper methods which we should update, to make them compatible with the messenger
Create a missing partial file
Create a missing partial file
Update another method
Update another method
Create the partial file
Create the partial file
Test the helper methods with specs on your own.
Test the helper methods with specs on your own.
Commit the changes
Commit the changes
git add -A
git commit -m "
- Update the append_previous_messages_partial_path helper method in
Shared::MessagesHelper
- Update the replace_link_to_private_messages_partial_path method in
Private::MessagesHelper"
Now after an initial load messages link click, the app will automatically keep loading previous messages until there is a scroll bar on the messages list. To make the initial click happen, add some JavaScript:
Now after an initial load messages link click, the app will automatically keep loading previous messages until there is a scroll bar on the messages list. To make the initial click happen, add some JavaScript:
When you visit the /messenger
path, you see the messenger:
When you visit the /messenger
path, you see the messenger:
Then you can open any of your conversations.
Then you can open any of your conversations.
Commit the changes.
Commit the changes.
Now on smaller screens, when users click on the navigation bar’s link to open a conversation, their conversation should be opened inside the messenger instead of a conversation window. To make this possible, we’ve to create different links for smaller screens.
Now on smaller screens, when users click on the navigation bar's link to open a conversation, their conversation should be opened inside the messenger instead of a conversation window. To make this possible, we've to create different links for smaller screens.
Inside the navigation’s _private.html.erb
partial, which stores a link to open a private conversation, add an additional link for smaller screen devices. Add this link just below the open_private_conversation_path
path’s link in the file
Inside the navigation's _private.html.erb
partial, which stores a link to open a private conversation, add an additional link for smaller screen devices. Add this link just below the open_private_conversation_path
path's link in the file
On smaller screens, this link is going to be shown instead of the previous one, dedicated for bigger screens. Add an additional link to open group conversations too
On smaller screens, this link is going to be shown instead of the previous one, dedicated for bigger screens. Add an additional link to open group conversations too
The reason why we see different links on different screen sizes is that previously we’ve set CSS for bigger-screen-link
and smaller-screen-link
classes.
The reason why we see different links on different screen sizes is that previously we've set CSS for bigger-screen-link
and smaller-screen-link
classes.
Commit the changes.
Commit the changes.
git add -A
git commit -m "Inside _private.html.erb and _group.html.erb, in the
layouts/navigation/header/dropdowns/conversations, add alternative
links for smaller devices to open conversations"
Messenger’s versions on desktop and mobile devices are going to differ a little bit. Write some JavaScript inside the messenger.js
, so after a user clicks to open a conversation, js will determine to show a mobile version or not.
Messenger's versions on desktop and mobile devices are going to differ a little bit. Write some JavaScript inside the messenger.js
, so after a user clicks to open a conversation, js will determine to show a mobile version or not.
Now when you open a conversation on a mobile device, it looks like this
Now when you open a conversation on a mobile device, it looks like this
Commit the change.
Commit the change.
git add -A
git commit -m "Add JavaScript to messenger.js to show a different
messenger's version on mobile devices"
Now make group conversations functional on the messenger. Majority of work with the messenger is already done, so setting up group conversations is going to be much easier. If you look back inside the MessengersController
, we have the get_group_conversation
action. Create a template file for it:
Now make group conversations functional on the messenger. Majority of work with the messenger is already done, so setting up group conversations is going to be much easier. If you look back inside the MessengersController
, we have the get_group_conversation
action. Create a template file for it:
Then create a file to render a group conversation in the messenger:
Then create a file to render a group conversation in the messenger:
Create its partials:
Create its partials:
Commit the changes:
Commit the changes:
git add -A
git commit -m "Create a get_group_conversation.js.erb template and
its partials inside the messengers"
That’s what group conversations in the messenger look like:
That's what group conversations in the messenger look like:
5. Notifications (5. Notifications)
The application already has all fundamental features ready. In this section we’ll put our energy on enhancing those vital features. Instantaneous notifications, when other users try to get in touch with you, provide a better user experience. Let’s make users aware whenever they get a contact request update or joined to a group conversation.
The application already has all fundamental features ready. In this section we'll put our energy on enhancing those vital features. Instantaneous notifications, when other users try to get in touch with you, provide a better user experience. Let's make users aware whenever they get a contact request update or joined to a group conversation.
Contact requests (Contact requests)
Generate a notification channel which will handle all user’s notifications.
Generate a notification channel which will handle all user's notifications.
rails g channel notification
Commit the change.
Commit the change.
git add -A
git commit -m "Create a NotificationChannel"
Every user is going to have its own unique notification channel. Then we have the ContactRequestBroadcastJob
, which will broadcast contact requests and responses.
Every user is going to have its own unique notification channel. Then we have the ContactRequestBroadcastJob
, which will broadcast contact requests and responses.
Generate the job.
Generate the job.
rails g job contact_request_broadcast
Create a _contact_request.html.erb
partial, which will be used to add contact requests in the navigation bar’s drop down menu. In this case we’ll add those requests dynamically with the ContactRequestBroadcastJob
Create a _contact_request.html.erb
partial, which will be used to add contact requests in the navigation bar's drop down menu. In this case we'll add those requests dynamically with the ContactRequestBroadcastJob
Fire the job every time a new Contact
record is created:
Fire the job every time a new Contact
record is created:
Commit the changes.
Commit the changes.
git add -A
git commit -m "Create a ContactRequestBroadcastJob"
Then create a drop down menu itself on the navigation bar:
Then create a drop down menu itself on the navigation bar:
Define the nav_contact_requests_partial_path
helper method:
Define the nav_contact_requests_partial_path
helper method:
Wrap the method with specs and then create the partial files:
Wrap the method with specs and then create the partial files:
Inside the _dropdowns.html.erb
file, render the _contact_requests.html.erb
, just below the conversations. So we could see a drop down menu of contacts’ requests on the navigation bar
Inside the _dropdowns.html.erb
file, render the _contact_requests.html.erb
, just below the conversations. So we could see a drop down menu of contacts' requests on the navigation bar
Commit the changes.
Commit the changes.
git add -A
git commit -m "
- Create a _contact_requests.html.erb inside the
layouts/navigation/header/dropdowns
- Define a nav_contact_requests_partial_path in NavigationHelper"
Also create a partial file for a single contact request:
Also create a partial file for a single contact request:
Commit the change.
Commit the change.
git add -A
git commit -m "Create a _request.html.erb inside the
layouts/navigation/header/dropdowns"
Add CSS to style and position the contact requests’ drop down menu:
Add CSS to style and position the contact requests' drop down menu:
Commit the changes.
Commit the changes.
git add -A
git commit -m "Add CSS in navigation.scss to style and position the
contact requests drop down menu"
On the navigation bar, we can see a drop down menu for contact requests now.
On the navigation bar, we can see a drop down menu for contact requests now.
We have the notifications channel and the job to broadcast contact requests’ updates. Now we need to create a connection on the client side, so users could send and receive data in real time.
We have the notifications channel and the job to broadcast contact requests' updates. Now we need to create a connection on the client side, so users could send and receive data in real time.
Notice that if
statements, where a contact request was accepted and declined, have empty code blocks. You can play around and add your own code here.
Notice that if
statements, where a contact request was accepted and declined, have empty code blocks. You can play around and add your own code here.
Also create a contact_requests.js
file to perform DOM changes after certain events and broadcast performed actions to the opposed user, using the contact_request_response
callback function
Also create a contact_requests.js
file to perform DOM changes after certain events and broadcast performed actions to the opposed user, using the contact_request_response
callback function
Also after a new contact request is sent from a conversation window, remove the option to send the request again. Inside the conversation’s options.js
file, add the following:
Also after a new contact request is sent from a conversation window, remove the option to send the request again. Inside the conversation's options.js
file, add the following:
Now contact requests are going to be handled in real time and the user interface will be changed after particular events.
Now contact requests are going to be handled in real time and the user interface will be changed after particular events.
Thanks to Toni Shortsleeve.
Thanks to Toni Shortsleeve.
翻译自: https://www.freecodecamp.org/news/lets-create-an-intermediate-level-ruby-on-rails-application-d7c6e997c63f/
ruby on rails