I am jumping into Sails.js again and working through to create flash messages throughout my app (for errors, successes, or alerts). I was looking for a good way to do it and found this discussion, I implemented the solution they had suggested.
The general mechanism works great, however, the flash message is only seen after a secondary refresh or after another post. It does not show at first upon page load. Here is how I have everything structured and I am using "sails": "~0.10.0-rc7" currently:
In my api/policies folder, I have flash.js:
// flash.js policy
module.exports = function(req, res, next) {
res.locals.messages = {
success: [],
error: [],
warning: []
};
if(!req.session.messages) {
req.session.messages = { success: [], error: [], warning: [] };
return next();
}
res.locals.messages = _.clone(req.session.messages);
// Clear flash
req.session.messages = { success: [], error: [], warning: [] };
return next();
};
In my api/services, I have FlashService.js:
// FlashService.js
module.exports = {
success: function(req, message) {
req.session.messages['success'].push(message);
},
warning: function(req, message) {
req.session.messages['warning'].push(message);
},
error: function(req, message) {
req.session.messages['error'].push(message);
}
}
My config/policies.js is also configured with the flash policy:
// config/policies.js
module.exports.policies = {
'*': [true, 'flash'],
'UserController': {
'join': ['flash'],
},
};
Now, with all that setup, an example of how I am using it is in my UserController for my join action:
module.exports = {
join: function(req, res) {
// If username, email, and password compare come back true: create user.
if(newUser.username && newUser.email && newUser.password == newUser.confirmPassword) {
// Logic to create user.
res.view();
// If not, then report an issue.
} else {
FlashService.error(req, 'There was an issue.');
res.view();
};
}
};
Finally, my view is exactly the same code as that discussion I linked. I am using EJS on this:
<% if (messages && messages['error'].length > 0) { %>
<div class="alert alert-danger">
<% messages['error'].forEach(function(message) { %>
<%= message %>
<br>
<% }); %>
</div>
<br>
<% } %>
<% if (messages && messages['warning'].length > 0) { %>
<div class="alert alert-warning">
<% messages['warning'].forEach(function(message) { %>
<%= message %>
<br>
<% }); %>
</div>
<br>
<% } %>
<% if (messages && messages['success'].length > 0) { %>
<div class="alert alert-success">
<% messages['success'].forEach(function(message) { %>
<%= message %>
<br>
<% }); %>
</div>
<br>
<% } %>
What could I be doing wrong? Any help would be much appreciated!
Thanks,
Your messages are being read before the the controller is executed (and just message is set after it is read). The req object is available in your view, you should just read req.session.messages['xxxx'] directly into your view.
Related
So I just started learning Node.js and I'm trying to figure out how to load more content on the button click. I think I'm either misunderstanding something or I'm confusing myself.
The code might be a bit of a mess at the moment as I've been trying a bunch of different things.
So far I have a homepage route:
//Home
app.get('/', (req, res)=> {
res.render('homepage');
});
On the homepage, I have a search bar that calls the /search route with get:
<form action="/search" method="get" id="form">
<input class="search" name="searchQuery"/>
<button type="submit">Submit</button>
</form>
Then the search route which makes a call to the API, parses the XML, renders the search page, and sends some data:
//Search
app.get('/search', async (req, res) => {
g_searchQuery = req.query.searchQuery;
const fetch_response = async (url, query) => {
try {
const response = await fetch(url);
const xml = await response.text();
xml2js.parseString(xml, (error, result) => {
var paginations = result.GoodreadsResponse.search[0];
totalResults = parseInt(paginations['total-results']);
resultsEnd = parseInt(paginations['results-end']);
g_resultsEnd = resultsEnd;
var test = result.GoodreadsResponse.search[0].results[0].work.map( w => {
let bestBook = w.best_book[0];
return {
title: bestBook.title[0],
author: bestBook.author[0].name,
image_url: bestBook.image_url[0],
small_image: bestBook.small_image_url[0],
original_publication_year: w.original_publication_year[0]._,
}
});
res.render('search', {data : {
goodReadsResponse : test,
searchQuery : query,
totalResults : totalResults,
resultsEnd : g_resultsEnd,
currentPage : g_currentPage
}});
res.end();
});
}
};
fetch_response(goodreadsapi + '?key=' + goodreadskey + '&q=' + g_searchQuery + '&page=' + g_currentPage, g_searchQuery);
});
The search page renders the data and has a load more button which with Fetch api makes a call to /search POST:
<body>
<h1>You searched for: <%= data.searchQuery %></h1>
<% if(data.goodReadsResponse) { %>
<ul class="data-container">
<%= data.totalResults %>
<%= data.currentPage %>
<% data.goodReadsResponse.forEach(function(book) { %>
<li>
<span><%= book.title %></span>
<span><%= book.author %></span>
<span><img src="<%= book.image_url %>"/></span>
<span><img src="<%= book.small_image %>"/></span>
<span><%= book.original_publication_year %></span>
</li>
<% }); %>
</ul>
<% if(data.resultsEnd <= data.totalResults) { %>
<div class="load-more__container">
<button class="load-more__button" type="button">Load More</button>
</div>
<% } %>
<% } %>
</body>
<script type="text/javascript">
window.addEventListener('load', function() {
const loadMore = document.querySelector('.load-more__button');
if(loadMore) {
loadMore.addEventListener('click', (event) => {
event.preventDefault('Load More');
fetch('/search', {
method : 'POST'
})
});
}
});
</script>
Then this is the part where I get stuck and confused. I have a app.post /search route for the load more button but I'm not sure whats the best way to pass data to that route/if I even need to.
My thinking is that I need to pass most of the data I passed through the render in /search app.get route to the app.post /search route add 1 to the current page, then append the data that I get from the API to the previous data then render the page with that updated data. But I'm slightly stuck.
Here is my repo for this, it's not 1:1 since I tried to clean some things up for the question.
Any help would be greatly appreciated,
Thank you.
let bestBook = w.best_book[0];
In the above line, Considering w.best_book is a list of books, u could just loop it and save all the books in a seperate variable, and render it when Load More button is pressed.
I am currently learning to build a blog website using Node.js, Express and ejs
I got the following error when I try to render the "show" page
I found a similar problem here but there is a different usage of my code, I still couldn't get mine solved.
The similar problem
I did also had an same type error earlier says "cannot read property 'username' of undefined" , but after I restart the server it gives me a different error.
The error I got now:
TypeError: C:\Users\kevin\Desktop\MyBlog\views\blogs\show.ejs:22
20|
21| <div class="text-muted">
22| <%= blog.date.toLocaleDateString()%> By <%= blog.author.displayName %> **<------**
23| </div>
24| </div>
25|
Cannot read property 'toLocaleDateString' of undefined
Portion of router.js:
//show the specific blog
router.get("/home/:slug", async(req, res) => {
const blog = await Blog.find({ slug: req.params.slug });
console.log(blog);
if (blog == null) {
res.redirect("/home");
}
res.render("blogs/show", {blog: blog});
})
Portion of show.ejs
<div class="card-header">
<h1 class="mb-1">
<%= blog.title %>
<i class="fas fa-home fa-lg"></i>
<!-- Override DELETE method -->
<form action="/home/<%= blog.id %>?_method=DELETE" method="POST" class="d-inline">
<% if((currentUser) && (currentUser.username === blog.author.username)){ %>
<span class="far fa-edit fa-lg"></span>
<button type="submit" class="btn btn-light btn-sm float-right"><i class="fas fa-trash-alt fa-lg"></i></button>
<% } %>
</form>
</h1>
<div class="text-muted">
<%= blog.date.toLocaleDateString()%> By <%= blog.author.displayName %>
</div>
</div>
I checked that the blog object is saved into the database correctly:
{
"_id" : ObjectId("5f4722cc1a623a7710871d6d"),
"author" : {
"id" : ObjectId("5f47177faa8e6e3e4c80f3e9"),
"username" : "333#gmail.com",
"displayName" : "Jacky Chan"
},
"date" : ISODate("2020-08-27T03:04:44.646Z"),
"title" : "1321321321321",
"coverImg" : "",
"contents" : "<p>3213131321321321</p>",
"slug" : "1321321321321",
"sanitizedHtml" : "<p>3213131321321321</p>",
"__v" : 0
}
I used console.log in the ejs file and it seems that it did found the correct blog....
I am guessing that the blog object is referenced incorrectly, maybe?
Finally I don't know if it is the problem of async/await but I tried all I can which eventually lead me here...
Any help will be appreciated! Thanks in advance!
Update 1:
Thanks to #AdamExchange
I tried the answer from #AdamExchange that switched find() to findOne().
The previous error is gone but there is a new error that when I use my create new blog route, it is calling the show route and trying to find the blog.
New Error:
TypeError: C:\Users\kevin\Desktop\MyBlog\views\blogs\show.ejs:9
7| <div class="card-header">
8| <h1 class="mb-1">
9| <%= blog.title %>
10| <i class="fas fa-home fa-lg"></i>
11| <!-- Override DELETE method -->
12| <form action="/home/<%= blog.id %>?_method=DELETE"
method="POST" class="d-inline">
Cannot read property 'title' of null
My question is why would the show route get involved when I use the GET method on create routes? (I do have a button on the create blog page that uses the show route)
Here I will post my entire router hopefully that will help:
const express = require("express");
const router = express.Router({mergeParams:true});
const Blog = require("../models/blog");
const middleware = require("../middleware");
router.post("/home", (req, res, next) => {
req.blog = new Blog();
next()
}, saveBlogAndRedirect('new'))
router.get("/home/new", middleware.isLoggedIn, async(req, res) => {
let blog = new Blog();
res.render("blogs/new", { blog : blog });
})
//Get the blog with the correctid
router.get("/home/edit/:id", async(req, res) => {
const blog = await Blog.findById(req.params.id);
res.render("blogs/edit", { blog : blog });
})
router.put("/home/:id", async(req, res, next) => {
req.blog = await Blog.findById(req.params.id);
next()
}, saveBlogAndRedirect('edit'))
//show the specific blog
router.get("/home/:slug", async(req, res) => {
const blog = await Blog.findOne({ slug: req.params.slug });
if (blog == null) {
res.redirect("/home");
}
res.render("blogs/show", {blog: blog});
})
router.delete("/home/:id", async(req, res) => {
await Blog.findByIdAndDelete(req.params.id)
res.redirect("/home");
})
function saveBlogAndRedirect(path){
return async(req, res) => {
let blog = req.blog;
blog.title = req.body.title;
blog.coverImg = req.body.coverImg;
blog.contents = req.body.contents;
blog.author = {
id : req.user._id,
username: req.user.username,
displayName: req.user.displayName
}
try{
blog = await blog.save();
res.redirect(`/home/${blog.slug}`);
}
catch (e) {
console.log(e)
res.render(`blogs/${path}`, { blog : blog });
}
}
}
module.exports = router;
const blog = await Blog.find({ slug: req.params.slug });
Will return an array of the blogs with that slug. If you know there is only one, you can change it to .findOne. The array of blogs is getting passed to the ejs template as blog (noted by your screenshot
[
{
author: ...
}
]
The 'date' property of the array is meaningless, but if you make sure to only pass your ejs template one blog object, that should fix it
Updated your code for, it will also work if the number of document is more than one. Since you are using find() method it will return all the document with matching filter.
Router
//show the specific blog
router.get("/home/:slug", async(req, res) => {
const blog = await Blog.find({ slug: req.params.slug });
console.log(blog);
if(blog == null) {
res.redirect("/home");
}
res.render("blogs/show", {blog:blog});
});
ejs section
<% for(var i=0; i<blog.length; i++){ %>
<div class="card-header">
<h1 class="mb-1">
<%= blog[i].title %>
<i class="fas fa-home fa-lg"></i>
<!-- Override DELETE method -->
<form action="/home/<%= blog[i].id %>?_method=DELETE" method="POST" class="d-inline">
<% if((currentUser) && (currentUser.username === blog[i].author.username)){ %>
<span class="far fa-edit fa-lg"></span>
<button type="submit" class="btn btn-light btn-sm float-right"><i class="fas fa-trash-alt fa-lg"></i></button>
<% } %>
</form>
</h1>
<div class="text-muted">
<%= blog[i].date.toLocaleDateString()%> By <%= blog[i].author.displayName %>
</div>
</div>
<% } %>
I am learning Web development by making projects. I am trying to display Login and Logout button based on the login status of the user on my website. I am using ExpressJs, PassportJs and EJS. I tried this but I am getting error.
EJS code:
<form class="form-inline mt-2 mt-md-0">
<% if (login_info) { %>
Logout
<% } %>
<% else { %>
Login
<% } %>
</form>
Server Side Code:
router.get('/dashboard', function (req,res){
if (!req.user) {
res.render('dashboard', { login_info: 'false' });
}
else {
res.render('dashboard', { login_info: 'true' });
}
});
Error
SyntaxError: Unexpected token 'else' in C:\Users\nodeapp\views\dashboard.ejs while compiling ejs
Is there anything I am doing wrong here?
Thanks
You are initializing value of login_info as string not as boolean. Can you check if it works
router.get('/dashboard', function (req,res){
if (!req.user) {
res.render('dashboard', { login_info:false });
}
else {
res.render('dashboard', { login_info:true });
}
});
And on your ejs keep } else on same line as
<% if (true) { %>
<p> valid!!!</p>
<%} else { %>
<p> Not Allowed </p>
<% } %>
I keep getting the "Cannot charge a user that has no active card" error when trying to bill a test user.
The app works so that a user gets charged $10 on sign up after they input their CC credentials.
I know the customers are being created but the charges aren't being sent
Here is the Stripe error
My credit_card_form.js file which is what collects the credit card info and sends it to stripe without it hitting my database.
$(document).ready(function(){
var show_error, stripeResponseHandler, submitHandler;
submitHandler = function (event) {
var $form = $(event.target);
$form.find("input[type=submit]").prop("disabled", true);
//If Stripe was initialized correctly this will create a token using the credit card info
if(Stripe){
Stripe.card.createToken($form, stripeResponseHandler);
show_error("CC token generated")
} else {
show_error("Failed to load credit card processing functionality. Please reload this page in your browser.")
}
return false;
};
// calling the SUBMIT
$(".cc_form").on('submit', submitHandler);
// RESPONSE HANDLER
stripeResponseHandler = function (status, response) {
var token, $form;
$form = $('.cc_form');
if (response.error) {
console.log(response.error.message);
show_error(response.error.message);
$form.find("input[type=submit]").prop("disabled", false);
} else {
token = response.id;
$form.append($("<input type=\"hidden\" name=\"payment[token]\" />").val(token));
$("[data-stripe=number]").remove();
$("[data-stripe=cvv]").remove();
$("[data-stripe=exp-year]").remove();
$("[data-stripe=exp-month]").remove();
$("[data-stripe=label]").remove();
$form.get(0).submit();
}
return false;
};
//ERROR HANDLING
show_error = function (message) {
if($("#flash-messages").size() < 1){
$('div.container.main div:first').prepend("<div id='flash-messages'></div>")
}
$("#flash-messages").html('<div class="alert alert-warning"><a class="close" data-dismiss="alert">×</a><div id="flash_alert">' + message + '</div></div>');
$('.alert').delay(5000).fadeOut(3000);
return false;
};
});
And payment.rb
class Payment < ApplicationRecord
attr_accessor :card_number, :card_cvv, :card_expires_month, :card_expires_year
belongs_to :user
def self.month_options
Date::MONTHNAMES.compact.each_with_index.map{|name,i| ["#{i+1} - #{name}", i+1]}
end
def self.year_options
(Date.today.year..(Date.today.year+10)).to_a
end
def process_payment
customer = Stripe::Customer.create email:email, card:token
Stripe::Charge.create customer:customer.id, amount: 1000, description: "Premium", currency: "usd"
end
end
And a snippet of the devise registrations new.html.erb which is where the user will be charged from
<div class="col-md-3">
<%=p .select :card_expires_month, options_for_select(Payment.month_options), {include_blank: "Month"}, "data-stripe"=>"exp-month", class: "form-control", required: true%>
</div>
<div class="col-md-3">
<%=p .select :card_expires_year, options_for_select(Payment.year_options.push), {include_blank: "Year"}, class: "form-control", data: {stripe: "exp-year"}, required: true%>
</div>
I know that the customers are being created but I have no clue why my customers aren't paying when I use the test credit card numbers. No answers on stack overflow have helped me as of yet.
Jack and I had a chat and discovered that Stripe.js wasn't loading, so the token wasn't being sent in the request.
The issue was <%= javascript_include_tag 'https://js/stripe.com/v2' %> being a mistyped js link in application.html.erb
The fix was making it <%= javascript_include_tag 'https://js.stripe.com/v2/' %>
I'm having some trouble getting this table to load properly because the page is loading before all the information is passed to my ejs template. Pretty new to all of this and would appreciate any help!
I should note that owneditems is an array of IDs in the user schema.
routes.js:
app.get('/profile/:username', function(req, res) {
User.findOne({username: req.params.username}, function(err, user) {
var newDocs = [];
if (!user) {
req.flash('profilemessage', 'No such user exists.');
} else {
user.owneditems.map(function(i) {
Items.findById(mongoose.Types.ObjectId(i), function(err, idoc) {
newDocs.push("<tr><td>" + idoc.name + "</td><td>" + idoc.brand</td></tr>");
});
});
}
res.render('profile.ejs', {title: 'Profile', items: newDocs, message: req.flash('profilemessage')});
});
});
Profile.ejs:
<!-- content -->
<div class="wrapper row2">
<div id="container" class="clear">
<section>
<% if (message) { %>
<h4><%= message %></h4>
<% } %>
<table id="owneditems" class="sortable">
<tr><th>Name</th><th>Brand</th></tr>
<% for(var i=0; i<items.length; i++) {%>
<%- items[i] %>
<% } %>
</table>
</section>
</div>
</div>
<% include layoutBottom %>
This type of setup works for me on another page, I just can't get it working here. Thanks!
The reason why the page is rendered before information is loaded is becauseItems.findById is asynchronous. This means newDocs will not return the array of items you're expecting when it's passed to res.render.
When you want to load (arrays of) subdocuments with Mongoose, it's best to use query#populate. This method will allow you to swap out the item IDs in your user.owneditems array for the actual item document in one go.
I think this would work in your case:
app.get('/profile/:username', function(req, res) {
User.findOne({username: req.params.username})
.populate('owneditems')
.exec(function(err, user) {
var newDocs = [];
if (!user) {
req.flash('profilemessage', 'No such user exists.');
} else {
user.owneditems.forEach(function(i) {
newDocs.push("<tr><td>" + i.name + "</td><td>" + i.brand</td></tr>");
});
}
res.render('profile.ejs', {title: 'Profile', items: newDocs, message: req.flash('profilemessage')});
});
});
Also note I switched map with forEach (which is what it seems you're going for given your callback)