Building A Content Management System Using The MEAN Stack - 8 (Front-End Development)
Repository
https://github.com/nodejs/node
What Will I Learn
The codebase for this tutorial is based on MEANie an open source content management system by Jason Watmore.
This is the eighth in the series of tutorials on building a content management system using the MEAN technology.
In the last tutorial we created the pages
, redirects
and account
view for the admin area.
In this tutorial we will start work on the blog section of the CMS.
The blog front end has a number of modules for outputting posts
, pages
, contact form
, post archive
and some more.
In this tutorial we will learn how to set up the home view in order to output posts on the blog, we will also be learning how to create the various states needed for the other features of the blog section.
N.B;- LINK TO THE EARLIER TUTORIALS IN THIS SERIES CAN BE FOUND AT THE END OF THIS POST
Requirements
- NodeJS and NPM,
- Angular
- MongoDB
- Code Editor
Difficulty
- Intermediate
Tutorial Contents
We created a client
folder to hold the code containing all front end functionalities. In the client
folder we will add a new directory blog
.
All the code in this tutorial and some subsequent ones will be added in this directory.
Custom CSS Styles
Before starting on the actual layout we need to add a css
file containing the custom styling for this section.
To add the css
file create a new directory named css
in the blog
directory and add the file style.css
In the style.css
file add the following code
.section-header {
height: 135px;
}
.nav-list {
height: 52px !important;
margin-top: 18px !important;
}
.nav-item {
height: 52px !important;
margin-bottom: 10px;
font-family: oswald;
}
.blog-header {
height: 300px;
background-image: url('../_content/images/blog-header.jpg');
padding-top: 70px;
}
.section-body {
margin-top: 30px;
padding: 50px;
}
.side-widget {
width: 100%;
}
.side-widget-content {
width: 100%;
padding-top: 20px;
padding-bottom: 20px;
}
.side-widget-area {
margin-bottom: 40px;
}
.post-area {
font-family: oswald !important;
font-size: 105%;
}
.post-publishdate {
margin-left: 5px;
margin-right: 5px;
border-radius: 10px;
}
.post-tag-slug {
width: 300px !important;
margin-left: 5px;
margin-right: 5px;
border-radius: 10px;
}
.post-tag {
margin-left: 5px;
margin-right: 5px;
word-spacing: 15px;
}
.post-metadata {
margin-left: 220px;
margin-top: 20px;
margin-bottom: 20px;
}
.post-title-area {
margin-bottom: 20px;
}
.post-summary-area {
margin-top: 20px;
}
.post-details-title {
font-family: oswald;
}
.share-post-area {
font-size: 20px;
}
.archive-details {
font-family: oswald;
}
.input-field {
margin-top: 50px !important;
margin-bottom: 50px !important;
}
.text-input {
height: 200px;
}
The above CSS comprises of all the custom styling needed for this section of the application.
index.html File
We need to create the main file that will render all other templates in the blog front end.
The main file is named index.html
and is created as a direct child in the blog directory.
The index.html
file will contain the main blog code. Paste the following in the code
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>MEAN-CMS - The MEAN Stack Blog</title>
<link rel="shortcut icon" href="/admin/_content/images/logo.png" type="image/png" />
<link rel="icon" href="/admin/_content/images/logo.png" type="image/png" />
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/css/materialize.min.css">
<link href="https://unpkg.com/[email protected]/dist/css/ionicons.min.css" rel="stylesheet">
<link href="http://fonts.googleapis.com/css?family=Oswald:400,300,700" rel="stylesheet">
<style>
<%- include('css/style.css') %>
</style>
</head>
<body>
<section class="section-header black">
<nav class="container black">
<a href="#" class="brand-logo"><span class="yellow-text">MEAN</span> <span class="white-text">CMS</span></a>
<br>
<ul class="center nav-list yellow">
<li><a href="/" class="black-text nav-item">Home</a></li>
<li><a href="/archive" class="black-text nav-item">Archive</a></li>
<li><a href="/contact" class="black-text nav-item">Contact</a></li>
</ul>
</nav>
</section>
<section class="blog-header center center-align">
<div class="container">
<h1 style="font-family: oswald"><span class="yellow-text">Lorem </span><span class="grey-text">Ipsum </span><span class="black-text">Blog</span></h1>
</div>
</section>
<section class="section-body grey lighten-3" ui-view>
<div class="row">
<div class="col l9 m9">
<% if(locals.templateUrl) { %>
<div ng-non-bindable>
<%- include(locals.templateUrl) %>
</div>
<% } %>
</div>
<div class="col l3 m3">
<div class="col l12 m12 side-widget-area">
<ul class="tabs">
<li class="tab center side-widget"><a href="#months-list" class="active center-align yellow white-text">Months</a></li>
</ul>
<div id="months-list" class="center center-align side-widget-content white yellow-text">
<%- include('_partials/month-list.html') %>
</div>
</div>
<div class="col l12 m12 side-widget-area">
<ul class="tabs">
<li class="tab center side-widget"><a href="#months-list" class="active center-align yellow white-text">Tags</a></li>
</ul>
<div id="months-list" class="center center-align side-widget-content white yellow-text">
<%- include('_partials/tag-list.html') %>
</div>
</div>
<div class="col l12 m12 side-widget-area">
<ul class="tabs">
<li class="tab center side-widget"><a href="#months-list" class="active center-align yellow white-text">Helpful Links</a></li>
</ul>
<div id="months-list" class="center center-align side-widget-content white">
<ul>
<li><a href="../admin" target="_blank" class="yellow-text">Admin Login</a></li>
</ul>
</div>
</div>
</div>
</div>
</section>
<footer>
<div class="center center-align copyright black">
<b><span class="white-text">Powered By </span><a href="#" target="_blank" class="yellow-text">Olatunde Oladunni - Web Application Developer</a></b>
</div>
</footer>
(html comment removed: syntax highlighter )
<script src="/_content/syntaxhighlighter/js/shCore.js"></script>
<script src="/_content/syntaxhighlighter/js/shBrushCSharp.js"></script>
<script src="/_content/syntaxhighlighter/js/shBrushCss.js"></script>
<script src="/_content/syntaxhighlighter/js/shBrushJScript.js"></script>
<script src="/_content/syntaxhighlighter/js/shBrushPlain.js"></script>
<script src="/_content/syntaxhighlighter/js/shBrushSql.js"></script>
<script src="/_content/syntaxhighlighter/js/shBrushVb.js"></script>
<script src="/_content/syntaxhighlighter/js/shBrushXml.js"></script>
<script>
SyntaxHighlighter.defaults['gutter'] = true;
SyntaxHighlighter.defaults['smart-tabs'] = true;
SyntaxHighlighter.defaults['auto-links'] = true;
SyntaxHighlighter.defaults['collapse'] = false;
SyntaxHighlighter.defaults['light'] = false;
SyntaxHighlighter.defaults['tab-size'] = 4;
SyntaxHighlighter.defaults['toolbar'] = true;
SyntaxHighlighter.defaults['wrap-lines'] = true;
</script>
(html comment removed: Minified JQuery )
<script src="https://code.jquery.com/jquery-3.3.1.min.js" integrity="sha256-FgpCb/KJQlLNfOu91ta32o/NMZxltwRo8QtmkMRdAu8=" crossorigin="anonymous"></script>
(html comment removed: Compiled and minified JavaScript )
<script src="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/js/materialize.min.js"></script>
<script>
$(document).ready(function(){
$('.tabs').tabs();
});
</script>
(html comment removed: angular )
<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.4.7/angular.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/angular-ui-router/0.2.15/angular-ui-router.min.js"></script>
(html comment removed: meanie )
<script src="_dist/app.min.js"></script>
</body>
</html>
In the index.html
file we use MaterializeCSS
and ionicons
for layout and icons respectively.
The font used was pulled from the Google fonts API and is known as Oswald
. It was pulled in with the line
<link href="http://fonts.googleapis.com/css?family=Oswald:400,300,700" rel="stylesheet">
We also included the custom css style added earlier by adding the following code in the <head></head>
tag
<style>
<%- include('css/style.css') %>
</style>
Instead of using the common <link />
element to import the css file we used the ejs
directive <%- include('css/style.css') %>
ejs
is a templating framework that allows javascript
code be embedded with html
code and we'll use it through out this section to infuse js
codes with the layout.
In the body
of our index.html
file we first create the navigation bar enclosed in the <section class="section-header black"></section>
element.
The class attribute value black
gives the entire navigation area a black background color.
The menu items in the in the navigation list are added as an unordered list.
There are three menu items in total and each is enclosed in a
<li><a href="" class="black-text nav-item"></a></li>
The first list item links to the home
page which is represented by href="/"
. /
is the default route set for the homepage in the app state.
The second list item links to the archive
page. The attribute value /archive
is the route url set for the archive
page in the app state.
The third list item links to the contact
page. The attribute value /contact
is the route url set for the archive page in the app state.
The main view for our app where other templates for this section will be rendered is enclosed in the element
<section class="section-body grey lighten-3" ui-view></section>
The angular ui-router
directive ui-view
indicates that this area will render all the views included in this section of the application.
angular ui-router
is imported towards the bottom of the index.html
file using the following line of code
<script src="//cdnjs.cloudflare.com/ajax/libs/angular-ui-router/0.2.15/angular-ui-router.min.js"></script>
In the main view area we create a new row with two columns.
The first column will output all the templates dynamically through the following code
<% if(locals.templateUrl) { %>
<div ng-non-bindable>
<%- include(locals.templateUrl) %>
</div>
<% } %>
The code above checks firstly checks if the view being requested exists in the app state.
If the view exists the template to that view is rendered inside the <div ng-non-bindable></div>
element.
The second column in the main view serves as a sidebar and it contains the following
- A list of posts filtered by the months they were created.
- A list of tags used in the posts on the blog
- Some utility links
All the constituents of the sidebar is contained in the element
<div class="col l12 m12 side-widget-area"></div>
All the constituents are separated and each is rendered through a materializecss tab element.
In the first tab we have the list of post filtered by their months of creation. The following code will render the filtered post list
<%- include('_partials/month-list.html') %>
In the second tab we have a list of tags used for the posts on the blog and it is rendered through the following code.
<%- include('_partials/tag-list.html') %>
In the third tab we have the utility links section which contains only one link at the moment. The link leads to the admin login page which is indicated through href="../admin"
in the <a></a>
element.
/admin
is the route url set for the admin area of the application in the app state.
At this point a screenshot of the homepage would look like the one below.
The index.html
file is still bare for a reason. The reason is because other templates needed for the file to display correctly hasn't been included in our application.
In order to fill the index.html
file we need to add the view templates for the blog features.
Before adding the view templates we will create the application state for the ui-router in order to register the routes for the different views.
Application State(app.js)
Create a new directory _dist
and add the file app.js
in the directory. The code in this file will manage the different states for all the views in this application.
Paste the following code in the file.
(function () {
'use strict';
angular
.module('app', ['ui.router', 'ngMessages'])
.config(config)
.run(run);
function config($locationProvider, $stateProvider, $urlRouterProvider, $httpProvider) {
// set variable to control behaviour on initial app load
window.initialLoad = true;
$locationProvider.html5Mode(true);
// default route
$urlRouterProvider.otherwise("/");
$stateProvider
.state('home', {
url: '/?:page',
templateUrl: function (stateParams) {
return window.initialLoad ? null :
'/?xhr=1' + (stateParams.page ? '&page=' + stateParams.page : '');
}
})
.state('post-details', {
url: '/post/:year/:month/:day/:slug',
templateUrl: function (stateParams) {
return window.initialLoad ? null :
'/post/' + stateParams.year + '/' + stateParams.month + '/' + stateParams.day + '/' + stateParams.slug + '?xhr=1';
}
})
.state('posts-for-tag', {
url: '/posts/tag/:tag',
templateUrl: function (stateParams) {
return window.initialLoad ? null :
'/posts/tag/' + stateParams.tag + '?xhr=1';
}
})
.state('posts-for-month', {
url: '/posts/:year/:month',
templateUrl: function (stateParams) {
return window.initialLoad ? null :
'/posts/' + stateParams.year + '/' + stateParams.month + '?xhr=1';
}
})
.state('page-details', {
url: '/page/:slug',
templateUrl: function (stateParams) {
return window.initialLoad ? null :
'/page/' + stateParams.slug + '?xhr=1';
}
})
.state('archive', {
url: '/archive',
templateUrl: '/archive?xhr=1'
})
.state('contact', {
url: '/contact',
templateUrl: '/contact?xhr=1',
controller: 'Contact.IndexController',
controllerAs: 'vm'
})
.state('contact-thanks', {
url: '/contact-thanks',
templateUrl: '/contact-thanks?xhr=1'
});
// mark all requests from angular as ajax requests
$httpProvider.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
}
function run($rootScope, $timeout, $location, $window) {
// initialise google analytics
$window.ga && $window.ga('create', 'UA-30211492-1', 'auto');
$rootScope.$on('$stateChangeSuccess', function () {
// hide mobile nav
$rootScope.showNav = false;
// track pageview
var urlWithoutHash = $location.url().split('#')[0];
$window.ga && $window.ga('send', 'pageview', urlWithoutHash);
// jump to top of page if not initial page load
if (!window.initialLoad) {
document.body.scrollTop = document.documentElement.scrollTop = 0;
}
$timeout(function () {
// run syntax highlighter plugin
SyntaxHighlighter.highlight();
});
window.initialLoad = false;
});
}
})();
We register the blog application with angular.module()
and include the directives ui.router
and ngMessages
ui.router
specifies that the application is using angular ui-router
module for routing operations in the application.
The ngMessages
directive will show or hide messages based on the state of the object each message listens to in the application.
We have a function config()
which is used to configure the states for the different routes and views.
In the config()
function we first of all set the initialLoad
property of the window
object to true
. This will help control the behavior of the application whenever the application loads for the first time.
We also set the html5mode
method of the $locationProvider
object to true
which will grant the application access to the HTML history API.
We set the default route for the application using $urlRouterProvider.otherwise("/");
which is the page that displays by default when no views are requested for by the application.
Using the $stateProvider
object we add the states for the different views of the application.
Each state and its properties are inserted in the state()
method.
We first of all create the state for the homepage view. The name of the state is home
.
The url
for the home view is set to '/?:page'
.
We have a templateUrl
for the home
view which is generated by a function.
The function uses a ternary operator to check the value of window.initialLoad
which we set earlier. If it returns true
the function returns a null
object but if it returns false the function returns an extended url which includes a string and parameters relating to the page being viewed.
The next state is for the view that displays each post detail like the post body.
The url
for this state is set to /post/:year/:month/:day/:slug
which comprises of the year, month and day that particular post was made in combination with the post slug.
The next state /posts/tag/:tag
uses post tags to filter through the posts and displays post created under a specifically requested tag.
The requested tag will be specified in the :tag
section of the url
.
After the posts-for-tag
we have the posts-for-month
state with its url set to /posts/:year/:month
.
posts-for-month
will only display posts created in a specific year and a specific month, both parameters are specified with :year
and :month
respectively.
page-details
will output the main content of any requested page.
The url for page-details
is /page/:slug
with :slug
being the slug associated with that specific page in the database.
The archive
state will display the archive view for all the posts on the blog on one page.
The url
for archive is set to /archive
.
contact
will output the view for the view for the contact from. Its route url is set to /contact
.
The contact
state also includes a controller module named Contact.IndexController
.
We are going to set all common http request headers
in the application to ajax requests
and that will be achieved through the following line
$httpProvider.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
Partials
The partials section will contain some files needed by the main view to output certain states of the app.
The partials files will be used to set the layout behavior of some area of the main view including the
- Posts filtered by their month of creation
- Full list of posts
- Posts filtered by their common tags
For the partials create a directory _partials
.
Month List(month-list.html)
In the _partials
directory create a new file month-list.html
.
This file will contain the code that will control the layout of posts filtered by their creation year and month.
Paste the following code in the file
<ul class="month-list">
<% years.forEach(function(year) { %>
<li>
<%= year.value %>
<ul>
<% year.months.forEach(function(month) { %>
<li>
<a href="/posts/<%= year.value%>/<%= month.value %>"><%= month.name %></a> (<%= month.postCount %>)
</li>
<% }) %>
</ul>
</li>
<% }) %>
</ul>
All the items from this file is enclosed in an unordered list.
Each list item in the list is displayed by firstly outputting the year from which the posts will be extracted, this is achieved with the line <%= year.value %>
.
This is followed by another unordered list and for each list item in this list we have a link element with the href
attribute "/posts/<%= year.value%>/<%= month.value %>"
.
The href
attribute above links to a list of posts associated with each month dynamically with each month url values determined by year.value
and month.value
depending on the year the link was clicked under.
Post List (post-list.html)
This file contains the layout code for displaying all blog posts on the home view of the application.
In the _partials
directory create a new file post-list.html
.
Paste the following code in the file
<div class="row">
<% posts.forEach(function(post) { %>
<div class="post-area">
<div class="row center">
<div class="col l12 m12 center center-align post-title-area">
<a href="<%= post.url %>">
<h5 class="center-align"><%= post.title %></h5>
</a>
</div>
<div class="post-metadata">
<div class="col l3 m3 post-publishdate yellow black-text center-align">
<%= post.publishDateFormatted %>
</div>
<div class="col l8 m8 yellow black-text center-align post-tag-slug">
<span class="left">Tags:</span>
<% var postTags = post.tags %>
<% postTags.forEach(function(tag) { %>
<span class="post-tag"><%= tag %></span>
<% }) %>
</div>
</div>
<div class="col l12 m12 post-summary-area">
<%= post.summary %>
</div>
</div>
</div>
<% }) %>
</div>
<% if(locals.pager && locals.pager.endPage > 1) { %>
<div class="post-pager">
<ul class="pagination">
<% if(locals.pager.currentPage > 1) { %>
<li class="first"><a href="?page=1">First</a></li>
<li class="previous"><a href="?page=<%= locals.pager.currentPage - 1 %>">Previous</a></li>
<% } else { %>
<li class="first disabled"><a>First</a></li>
<li class="previous disabled"><a>Previous</a></li>
<% } %>
<% for(var page = locals.pager.startPage; page <= locals.pager.endPage; page++) { %>
<li class="<%= page === locals.pager.currentPage ? 'active' : '' %>">
<a href="?page=<%= page %>"><%= page %></a>
</li>
<% } %>
<% if(locals.pager.currentPage < locals.pager.totalPages) { %>
<li class="next"><a href="?page=<%= locals.pager.currentPage + 1 %>">Next</a></li>
<li class="last"><a href="?page=<%= locals.pager.totalPages %>">Last</a></li>
<% } else { %>
<li class="next disabled"><a>Next</a></li>
<li class="last disabled"><a>Last</a></li>
<% } %>
</ul>
</div>
<% } %>
All the contents of these post list view is enclosed in <div></div>
tag which will create a new row
with different columns.
For each post to be displayed in the row we add another row, the first column to be displayed in this new row will contain the post title which will link to the post details.
The code for displaying the post title is
<a href="<%= post.url %>">
<h5 class="center-align"><%= post.title %></h5>
</a>
<%= post.url %>
will provide the link to the post details while <%= post.title %>
will supply the text for the post title.
In another column we output the date the post was published through the <%= post.publishDateFormatted %>
directive.
The next column displays the tags associated with the post gotten through the following code
<% var postTags = post.tags %>
<% postTags.forEach(function(tag) { %>
<span class="post-tag"><%= tag %></span>
<% }) %>
The code above creates adds every individual post tag in a span
tag in rder to define their behavior with css
.
The last column on the row outputs the post summary through the <%= post.summary %>
.
We also have a section for the pagination which sets the layout to make the post appear in a sequence of pages with each page containing the same number of posts as the last one.
The code for the pagination is wrapped in an if
statement which checks if the value for the variables locals.pager
and locals.pager.endPage
is greater than 1
.
locals.pager
represents the current page being displayed while locals.pager.endPage
represents the page where the post list ends.
If both evaluations are true a list will be displayed and in the list we have another if
statement which checks if the value of locals.pager.currentPage
is greater.
locals.pager.currentPage
also represents the current page being displayed and if the value is greater than one the list below will be displayed
<li class="first"><a href="?page=1">First</a></li>
<li class="previous"><a href="?page=<%= locals.pager.currentPage - 1 %>">Previous</a></li>
href="?page=1"
links to the first page on the list of pages containing the post list while href="?page=<%= locals.pager.currentPage - 1 %>
links to the page directly before the current page.
In any other case the list will be displayed only this time it will be unclickable due to it containing the disabled
class attribute.
Tag List (tag-list.html)
In order to set the layout for posts filtered by their common tags create a new file in the _partials
directory and add the file tag-list.html
<ol class="tag-list left-align">
<% tags.forEach(function(tag) { %>
<li>
<a href="/posts/tag/<%= tag.slug %>"><%= tag.text %></a>
</li>
<% }) %>
</ol>
In this file we have an ordered list and for each item of the ordered list and for each list item we output a link that leads to a view that displays the post associated with the listed tags.
In our next tutorial we will work on completing the home view in order to finally display all the contents of the _partials
directory on the homepage.
Curriculum
Building A Content Management System Using The MEAN Stack - 2(Create Controller Modules 1)
Building A Content Management System Using The MEAN Stack - 3 (Create Controller Modules 2)
Building A Content Management System Using The MEAN Stack - 4 (Create Services Modules)
Building A Content Management System Using The MEAN Stack - 5 (Front-End Development)
Building A Content Management System Using The MEAN Stack - 6 (Front-End Development)
Building A Content Management System Using The MEAN Stack - 7 (Front-End Development
Thank you for your contribution @gotgame.
We have reviewed your tutorial and suggested the following points:
The CSS code is not relevant to put in your tutorial. The reader when going to your github can visualize the CSS that you constructed.
Your tutorial is a bit confusing. Just explain the practical part and it is quite important in a tutorial to explain the theory about what will develop.
We are waiting for your next tutorial with our suggestions. Thank you for your work.
Your contribution has been evaluated according to Utopian policies and guidelines, as well as a predefined set of questions pertaining to the category.
To view those questions and the relevant answers related to your post, click here.
Need help? Write a ticket on https://support.utopian.io/.
Chat with us on Discord.
[utopian-moderator]
Need help? Write a ticket on https://support.utopian.io/.
Chat with us on Discord.
[utopian-moderator]
Thank you for your review, @portugalcoin!
So far this week you've reviewed 6 contributions. Keep up the good work!
Hi @gotgame!
Your post was upvoted by @steem-ua, new Steem dApp, using UserAuthority for algorithmic post curation!
Your post is eligible for our upvote, thanks to our collaboration with @utopian-io!
Feel free to join our @steem-ua Discord server
Hello! Your post has been resteemed and upvoted by @ilovecoding because we love coding! Keep up good work! Consider upvoting this comment to support the @ilovecoding and increase your future rewards! ^_^ Steem On!
Reply !stop to disable the comment. Thanks!
Hey, @gotgame!
Thanks for contributing on Utopian.
We’re already looking forward to your next contribution!
Get higher incentives and support Utopian.io!
Simply set @utopian.pay as a 5% (or higher) payout beneficiary on your contribution post (via SteemPlus or Steeditor).
Want to chat? Join us on Discord https://discord.gg/h52nFrV.
Vote for Utopian Witness!
As a follower of @followforupvotes this post has been randomly selected and upvoted! Enjoy your upvote and have a great day!