From the translator: “Elixir and Phoenix are a great example of where modern web development is heading. Already, these tools provide high-quality access to real-time technologies for web applications. Sites with increased interactivity, multiplayer browser games, microservices - those areas in which these technologies will serve a good service. The following is a translation of a series of 11 articles that describe in detail the aspects of development on the Phoenix framework that would seem such a trivial thing as a blog engine. But do not hurry to sulk, it will be really interesting, especially if the articles encourage you to pay attention to the Elixir or become its followers. ”
In this part we will talk more about layout than directly about Elixir, however, the article will be useful in that it will tell about the interaction with the Phoenix asset pipeline.
You can open the first part right now and create a working application on Phoenix in a few evenings and look at the Elixir in action. As well as get acquainted with web-sites, plug-ins, changesets and other scary words of functional web development :)
In the last part, we ended up writing tests for everything related to channels. This time there will be much less code on Elixir, instead of which we will learn how to use third-party libraries inside Phoenix applications. As a rule, Phoenix meets you with a very, very basic version of Bootstrap optimized for it. Let's move away a bit from this approach and replace it with another CSS framework (and will also use Sass).
A warning. The author of the article is definitely not a designer. The result may scare you, so try changing everything to your taste.
I would not like to start a debate on the topic of Bootstrap against the Foundation. Instead, let's just take Foundation, because the look of the site on it looks more interesting. Also for him you need to do a little less standard things, compared to Bootstrap. In addition, not many people tried to use the Foundation, so it will be so interesting!
$ npm install --save-dev foundation-sites motion-ui sass-brunch jquery@2.2.4 $ cp node_modules/foundation-sites/dist/foundation.min.js web/static/vendor/
Note. Please note that in order to avoid jQuery and Foundation compatibility issues, the specific jQuery version (v2.2.4) is installed here. For more information, see this article .
You also need to correct brunch-config.js
to accommodate the changes. Namely, connecting Foundation to Brunch, including loading the Sass-directories, configuring the Sass module, and installing jQuery as a global dependency for Npm.
exports.config = { // See http://brunch.io/#documentation for docs. files: { javascripts: { joinTo: "js/app.js" }, stylesheets: { joinTo: "css/app.css", order: { after: ["web/static/css/app.css"] // concat app.css last } }, templates: { joinTo: "js/app.js" } }, conventions: { // This option sets where we should place non-css and non-js assets in. // By default, we set this to "/web/static/assets". Files in this directory // will be copied to `paths.public`, which is "priv/static" by default. assets: /^(web\/static\/assets)/ }, // Phoenix paths configuration paths: { // Dependencies and current project directories to watch watched: [ "web/static", "test/static" ], // Where to compile files to public: "priv/static" }, // Configure your plugins plugins: { babel: { // Do not use ES6 compiler in vendor code ignore: [/web\/static\/vendor/] }, sass: { options: { includePaths: [ 'node_modules/foundation-sites/scss', 'node_modules/motion-ui/src', ] } } }, modules: { autoRequire: { "js/app.js": ["web/static/js/app"] } }, npm: { enabled: true, globals: { $: 'jquery', jQuery: 'jquery', } } };
Then we need to add a directory for Sass files. Create a web/static/scss
with 3 files inside:
application.scss
;_settings.scss
;_custom.scss
.In the web/static/scss/application.scss
file add:
// Only put imports and things here @import "settings"; @import "foundation"; @include foundation-everything; @import "motion-ui"; @include motion-ui-transitions; @import "custom";
In the file web/static/scss/_settings.scss
add the contents of the file by reference .
In the web/static/scss/_custom.scss
add:
// Put custom styles here $darker-gray: #2a2a2a; html, body { height: 100%; background-image: url("/images/computer-bg.png"); background-size: cover; background-repeat: no-repeat; background-color: #000; background-attachment: fixed; } .top-bar { background-color: rgba(0, 0, 0, 0.6); ul { background-color: inherit; padding-left: rem-calc(40); padding-right: rem-calc(40); } a { color: $white; } a:hover { color: $alert-color; } a.button:hover { color: $white; } .logo { padding-right: rem-calc(50); text-transform: uppercase; color: $white; font-weight: bolder; } } .image-overlay { margin-top: 10%; margin-bottom: 10%; } section.about { background: rgba(0, 0, 0, 0.6); color: $white; padding: rem-calc(50); .icon { text-align: center; font-size: rem-calc(64); margin-bottom: rem-calc(20); } h2 { text-align: center; } } section.more-info { background: rgba(0, 0, 0, 0.9); color: $white; min-height: 200px; padding: rem-calc(50); .social-media { font-size: rem-calc(48); a { color: white; } a:hover { color: $secondary-color; } } } section.main-content { margin-top: rem-calc(100); } .content { background: rgba(255, 255, 255, 0.8); padding: rem-calc(30); } footer { background-color: #000; color: $white; text-align: center; padding-bottom: rem-calc(20); font-size: rem-calc(14); } .post { background: rgba(255, 255, 255, 0.8); padding: rem-calc(30); margin-bottom: rem-calc(25); .title { text-align: center; border-bottom: 2px solid rgba(0, 0, 0, 0.6); margin-bottom: rem-calc(10); } }
You will also need to upload a picture and drop it into the web/static/assets/images/computer-bg.png
so that the style file can use it as a background image.
If you want to connect a font with icons from the Foundation set, follow these steps:
web/static/assets/fonts
directory: foundation-icons.eot
, foundation-icons.svg
, foundation-icons.ttf
, foundation-icons.woff
.web/static/css
directory: foundation-icons.css
.foundation-icons.css
file foundation-icons.css
paths for each font to the Phoenix foundation-icons.css
paths: /* Rest of the file */ @font-face { font-family: "foundation-icons"; src: url("/fonts/foundation-icons.eot"); src: url("/fonts/foundation-icons.eot?#iefix") format("embedded-opentype"), url("/fonts/foundation-icons.woff") format("woff"), url("/fonts/foundation-icons.ttf") format("truetype"), url("/fonts/foundation-icons.svg#fontcustom") format("svg"); font-weight: normal; font-style: normal; } /* Rest of the file */
Now icons must be fully installed.
Open the file web/static/css/phoenix.css
and delete everything that is in it! We do not need any of this, but it would be useful if we decided to use standard CSS.
Before continuing, I would like to do one more thing - to improve the code written earlier. For example, the way of working with the current user is a bit unreliable and requires you to write the same code over and over again. Let's add reusability to the code associated with the current user. First, create a new plug-in, which will be located in the web/controllers/current_user_plug.ex
file:
defmodule Pxblog.CurrentUserPlug do import Plug.Conn def init(default), do: default def call(conn, _opts) do if current_user = get_session(conn, :current_user) do assign(conn, :current_user, current_user) else conn end end end
What's going on here? First, Plug.Conn
imported into our module (so that you can easily access the assign
and get_session
). The following describes the init
function that accepts and then returns the default
variable. The very essence of the plug is in the call
function. Any plugin must implement two functions - init
and call
. The call
function must accept conn
and optional options, and return conn
(modified or not).
If current_user
is in the current session, then we get it and throw it inside the conn
so that you can get access to the current user from everywhere. If you can't get it, then just return the unmodified conn
. Then, to use the current_user
plug-in globally, open the web/router.ex
and paste it into the standard browser web/router.ex
:
pipeline :browser do plug :accepts, ["html"] plug :fetch_session plug :fetch_flash plug :protect_from_forgery plug :put_secure_browser_headers plug Pxblog.CurrentUserPlug end
That's all! Now, when we need to refer to the current user inside the markup, we can simply call @conn.assigns[:current_user]
. Wonderful!
We start refactoring of the main layout of the application to make it easier to make changes and easier to reuse common elements. Let's start with the head
section (everything between the <head>
tags). Create a web/templates/layout/head.html.eex
and drag the head
tag along with the contents into it:
<head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="description" content="A Tech Blog Written in Elixir and Phoenix"> <meta name="author" content="Brandon Richey"> <%= if current_user = @conn.assigns[:current_user] do %> <%= tag :meta, name: "channel_token", content: Phoenix.Token.sign(@conn, "user", current_user.id) %> <% end %> <title>Phoenix Tech Blog</title> <link rel="stylesheet" href="<%= static_path(@conn, "/css/app.css") %>"> </head>
For the most part, we simply copied the content of the head
tag, but at the same time, we slightly simplified working with the current user and added some information to the description
, author
and title
tags. Instead of the previous approach, when a helper from LayoutView
, it is now checked whether the current_user
inside assigns
, and if it is, the current user is taken as a parameter for the channel meta tag.
Now we go to refactoring the layout of the top navigation bar. Create a web/templates/layout/navigation.html.eex
file:
<div class="top-bar"> <div class="top-bar-left"> <ul class="menu"> <li class="logo"> Phoenix Tech Blog </li> <li> <%= link "Home", to: page_path(@conn, :index) %> </li> <li> <%= link "Posts", to: post_path(@conn, :index) %> </li> <%= if current_user = @conn.assigns[:current_user] do %> <li> <%= link "My Posts", to: user_post_path(@conn, :index, current_user.id) %> </li> <li> <%= link "New Post", to: user_post_path(@conn, :new, current_user.id) %> </li> <% end %> </ul> </div> <div class="top-bar-right"> <ul class="menu"> <%= if current_user = @conn.assigns[:current_user] do %> <li> <%= link current_user.username, to: user_path(@conn, :edit, current_user.id) %> </li> <li> <%= link "Logout", to: session_path(@conn, :delete, current_user.id), method: :delete, class: "button alert" %> </li> <% else %> <li> <%= link "Login", to: session_path(@conn, :new), class: "button" %> </li> <% end %> </li> </ul> </div> </div>
This is the first large piece of code in which Foundation is fully used. There is a class top-bar
, as well as various variations of the menu and the right / left shifts. We completely cut out the standard Phoenix logo and navigation bar and instead use plain text as the logo. For simplicity, we will call our project Phoenix Tech Blog . Also build some simple HTML with a couple of styles and components built into the Foundation. Once again, in any place where the old way of getting the current user is used, you need to replace it with a new approach using the plug.
The last example uses the following classes:
top-bar
- top navigation bar, as expected. ( Documentation )top-bar-left
- the sub-section of the top panel shifted to the left. top-bar-right - subsection shifted to the right.menu
- menu of links. ( Documentation )button
- style for the standard button. ( Documentation )alert
- the color of the button. ( Documentation )Now let's take a block of alerts, which are also quite often used code. Let's create a web/templates/layout/alerts.html.eex
file:
<div class="row alerts"> <%= if flash = get_flash(@conn, :info) do %> <div class="success callout alert-info" role="alert"> <%= flash %> </div> <% end %> <%= if flash = get_flash(@conn, :error) do %> <div class="alert callout alert-error" role="alert"> <%= flash %> </div> <% end %> </div>
Here we need to make a bit more edits, since Phoenix has specific expectations about the CSS classes used by default. These changes will make the warning block code and styles a bit more platform-independent.
If we can get a message from conn
, then accordingly display it inside a div
. If not, we do not display anything. An empty div
will still appear in the HTML source code, but it will not affect the display.
row
- the container for the grid system. ( Documentation )callout
- a standard container with a flash message. ( Documentation )success
- a nice light green style for a successful message. ( Documentation )alert
is a nice light red style for error messages. ( Documentation )We have 2 parls left that need to be improved. The first is the main content. Let's wrap the insides of the diva into the row
and content
classes:
<main role="main row content"> <%= render @view_module, @view_template, assigns %> </main>
The second is the template for the inclusion of standard Javascript code below. Create a file web/templates/layout/script.html.eex
:
<script src="<%= static_path(@conn, "/js/app.js") %>"></script> <script type="text/javascript"> $(function() { $(document).foundation(); }); </script>
The main refinement here is the closure that is enabled to call the foundation()
function on the document object. This is required for Foundation JS helpers built into Foundation.
Finally, let's take a look at the layout of the application after our refactoring and the general content. Let's change the web/templates/layout/app.html.eex
:
<!DOCTYPE html> <html lang="en"> <%= render "head.html", conn: @conn %> <body> <%= render "navigation.html", conn: @conn %> <section class="main-content row expanded"> <%= render "alerts.html", conn: @conn %> <div class="row content"> <%= render @view_module, @view_template, assigns %> </div> </section> <%= render "script.html", conn: @conn %> </body> </html>
As you can see, the layout of the application now looks much more accurate. It will be EXTREMELY convenient if we add a new layout for the index
action from the PageController
, which we call the "splash page", which means the welcome page.
Create a new web / templates / layout / splash.html.eex layout file:
<!DOCTYPE html> <html lang="en"> <%= render "head.html", conn: @conn %> <body> <%= render "navigation.html", conn: @conn %> <section class="main-content row expanded"> <%= render "alerts.html", conn: @conn %> <section class="text-center image-overlay row content"> <%= render @view_module, @view_template, assigns %> </section> <section class="about expanded row"> <div class="columns medium-12 large-4"> <div class="icon"> <i class="fi-like"></i> </div> <h2>What Powers This Blog</h2> <p> This blog is powered by <a href="http://elixir-lang.org">Elixir</a> and <a href="http://phoenixframework.org">Phoenix Framework</a> to give us a super fast, super clean, and super fun to modify blog engine! </p> </div> <div class="columns medium-12 large-4"> <div class="icon"> <i class="fi-comments"></i> </div> <h2>We Support Live Comments</h2> <p> By using the latest and greatest in client and server technology, we're able to offer a live, realtime commenting system with nearly no impact to performance! </p> </div> <div class="columns medium-12 large-4"> <div class="icon"> <i class="fi-pencil"></i> </div> <h2>We Support Markdown</h2> <p> We allow you to use <a href="https://daringfireball.net/projects/markdown/">Markdown</a> in all of your posts, which will make it simple to write up each post in the program of your choice and import it into this blog! </p> </div> </section> <section class="more-info expanded row"> This is an example design for the Phoenix Blog Engine project detailed in my series of <a href="https://medium.com/@diamondgfx/introduction-fe138ac6079d">Medium</a> posts. This design is merely a starting template; you may want to go out and find your own as well to make it look truly original! You're welcome to use the source code for this project as well as the design as you wish! If you're interested in more, please find me on the internet via: <br /> <div class="social-media text-center"> <a href="http://twitter.com/diamondgfx"><i class="fi-social-twitter"></i></a> <a href="https://medium.com/@diamondgfx/"><i class="fi-social-medium"></i></a> </div> </section> <footer class="expanded row"> I'm a standard footer for your site! Maybe you can throw a copyright © down here or something like that, or licensing! You know, whatever! </footer> </section> <%= render "script.html", conn: @conn %> </body> </html>
A lot of things are being done here, but in essence it’s just content with a couple of styles here and there. Let's look at a few more classes from the Foundation:
expanded
- expands the row
to the full width of the page. ( Documentation )text-center
- speaks for itself: aligns the text in the center. Similarly, text-left
and text-right
align the text left and right, respectively. ( Documentation )fi-comments
, fi-pencil
, fi-like
- icons from the Foundation set. ( Documentation )columns
- reports that we want to use the block as a separate column. ( Documentation )medium-12
, large-4
are examples of using an adaptive mesh model. On screens of size medium
layout will take 12 columns, and on screens of size large
only 4 columns. (You will also see an example of using small-N
, where N is a number between 1 and 12). ( Documentation )Finally, let Phoenix know that we will use the new layout for the welcome screen in PageController
. Open the file web/controllers/page_controller.ex
and add the following line:
plug :put_layout, "splash.html"
We also need to give guests the opportunity to view the latest N posts, as it would be foolish to make a cool blog that no one can read. First, open the web/router.ex
and add a resource for posts with a single index
action.
resources "/posts", PostController, only: [:index]
And then open the web/controllers/post_controller.ex
, change the index
function and add another index
function using pattern matching. But first, change the assign_user
at the top of the file:
plug :assign_user when not action in [:index]
Now change the index
function:
def index(conn, %{"user_id" => _user_id}) do conn = assign_user(conn, nil) if conn.assigns[:user] do posts = Repo.all(assoc(conn.assigns[:user], :posts)) |> Repo.preload(:user) render(conn, "index.html", posts: posts) else conn end end def index(conn, _params) do posts = Repo.all(from p in Post, limit: 5, order_by: [desc: :inserted_at], preload: [:user]) render(conn, "index.html", posts: posts) end
The first call checks for the presence of user_id
in the parameters. So we know that we get a certain post for a specific user. If we fall into this function, then the assign_user
must be reused to assign the user to conn
. In order not to load users for an empty array, we check in advance the presence of the user in assigns
. Otherwise, we simply return conn
, which will contain the redirection for a nonexistent user, a flash message and stop the request.
The second index
function is slightly different. We need to get the 5 most recent posts, so that we limit their number and preload users into each post. Do not worry about comments, because they do not need to be displayed on the page with a list of posts. Here you also need to slightly optimize the use of current_user
. To do this, open the file web/templates/post/index.html.eex
:
<div class="row"> <div class="small-10 columns"> <h1>Posts</h1> </div> <div class="small-2 columns text-right"> <%= if current_user = @conn.assigns[:current_user] do %> <%= link "New post", to: user_post_path(@conn, :new, current_user.id), class: "button expanded large" %> <% end %> </div> </div> <%= for post <- @posts do %> <div class="row post"> <div class="small-12 columns title"> <h3><%= post.title %></h3> </div> <div class="small-12 columns"> <small> Posted on <%= post.inserted_at %> by <%= link post.user.username, to: user_post_path(@conn, :index, post.user) %> </small> </div> <div class="small-12 columns"> <%= markdown(post.body) %> </div> <%= if current_user = @conn.assigns[:current_user] do %> <div class="small-12 medium-4 columns"> <%= link "Show", to: user_post_path(@conn, :show, post.user, post), class: "button expanded" %> </div> <div class="small-12 medium-4 columns"> <%= link "Edit", to: user_post_path(@conn, :edit, post.user, post), class: "button warning expanded" %> </div> <div class="small-12 medium-4 columns"> <%= link "Delete", to: user_post_path(@conn, :delete, post.user, post), method: :delete, data: [confirm: "Are you sure?"], class: "button alert expanded" %> </div> <% end %> </div> <% end %>
All that we did before - this is where they cleaned up the styles. Just in case, the code for these lessons is here , so you can look at all the changes you have made, if you want to have everything one-to-one.
We changed part of the markup and functionality, so the tests dropped. To fix them, open the file test/controllers/page_controller_test.exs
and change the check to “Phoenix Tech Blog”.
When everything is ready, you should see a similar welcome page:
This is what the guest will see when he decides to take a look at the list of posts:
Now the blog looks much more professional. In addition, we used Zurb Foundation 6 via Sass, as well as jQuery and a bunch of other small bonuses.
Hooray! Great work has been done, which has finally come to an end. Now everyone interested in Elixir and Phoenix has a pretty good introductory course that will help to get acquainted with the technology.
A few dozen articles on the Elixir in Russian you can find on the site of our project . - , - , . !
, , , . , , , , , , , , .
!
Source: https://habr.com/ru/post/335048/
All Articles