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>
<% } %>
Related
I'm trying to show "events" created by an "artist" on the artist's page but am running into error 'artist.events is not iterable'. Below is my 'artist' model (models-> artists.js):
const mongoose = require('mongoose');
const Schema = mongoose.Schema;
const passportlocalMongoose = require('passport-local-mongoose');
const artistSchema = new Schema({
email: {
type: String,
required: true,
unique: true
},
location: {
type: String,
required: [true, 'Hometown (so you can be paired with local venues)']
},
genre: {
type: String
},
joined_date: {
type: Date,
default: Date.now
},
about: String,
size: Number,
});
artistSchema.plugin(passportlocalMongoose);
module.exports = mongoose.model('Artist', artistSchema);
Next is my event model (which associates artists with event through the 'artist' array.
const mongoose = require('mongoose');
const Schema = mongoose.Schema;
const eventSchema = new Schema({
event_name: String,
location: String,
description: String,
image: String,
artist: {
type: Schema.Types.ObjectId,
ref: 'Artist'
},
});
module.exports = mongoose.model('Event', eventSchema);
Here is where I'm running into my actual problem - whey I try to list events under artists I hit the error. Below is the artist show page (views-> artists-> show.ejs)
<% layout('layouts/boilerplate') %>
<div class="row">
<div class="col-12">
<div class="card mb-3">
<img src="<%= artist.image %>" class="card-img-top" alt="...">
<div class="card-body">
<h5 class="card-title"><%= artist.username %></h5>
<p class="card-text">Genre: <%= artist.genre %></p>
</div>
<ul class="list-group list-group-flush">
<li class="list-group-item">Location: <%= artist.location %></li>
<li class="list-group-item">Number of people in group: <%= artist.size %></li>
<li class="list-group-item">About: <%= artist.about %></li>
</ul>
<% if (currentUser && artist.equals(currentUser._id)) {%>
<div class="card-body">
<a class="card-link btn btn-primary" href="/artists/<%=artist.id%>/edit">Edit</a>
<form class="d-inline" action="/artists/<%=artist.id%>?_method=DELETE" method="POST">
<button class="btn btn-primary">Delete</button>
<% } %>
</form>
<% for(let events of artist.events) { %>
<div class="class mb-3">
<p>Event name: <%= event.event_name %></p>
</div>
<% } %>
</div>
</div>
<div class="card-footer text-muted">
Back to
All Artists
</div>
</div>
</div>
Full error:
TypeError: /Users/chaseschlachter/mtapp/views/artists/show.ejs:22
20| <% } %>
21| </form>
>> 22| <% for(let events of artist.events) { %>
23| <div class="class mb-3">
24| <p>Event name: <%= event.event_name %></p>
25| </div>
artist.events is not iterable
Adding my artists routes for context (routes-> artists.js):
const express = require('express');
const router = express.Router();
const passport = require('passport');
const Artist = require('../models/artist');
const catchAsync = require('../utils/catchAsync');
const ExpressError = require('../utils/ExpressError');
/* lists artists from database */
router.get('/', async (req, res) => {
const artists = await Artist.find({});
res.render('artists/index', { artists })
});
router.get('/new', (req, res) => {
res.render('artists/new');
});
/* shows specific artists that exist in database */
router.get('/:id', catchAsync(async(req, res,) => {
const artist = await Artist.findById(req.params.id);
if (!artist) {
req.flash('error', 'Cannot find that Artist');
return res.redirect('/artists');
}
res.render('artists/show', { artist });
}));
/* artist edits form*/
router.get('/:id/edit', catchAsync(async (req, res) => {
const artist = await Artist.findById(req.params.id);
if (!artist) {
req.flash('error', 'Cannot find that Artist');
return res.redirect('/artists');
}
res.render('artists/edit', { artist });
}))
router.put('/:id', catchAsync(async (req, res) => {
const { id } = req.params;
const artist = await Artist.findByIdAndUpdate(id, { ...req.body.artist });
res.redirect(`/artists/${artist._id}`);
}))
What am I doing wrong?
Looking at your artistSchema, you haven't defined an events field so there isn't anything to iterate over. That field is simply not there inside the artist object, it is undefined.
Your artistSchema creates a collection of artist objects. These objects have only the fields you've supplied in your schema definition. Whereas you have another collection of event objects which are completely separate from artists defined by the eventSchema.
Since you would like to associate artists with events you have several options to do this:
Maintain a list of events as a array inside the artist schema (potentially of ObjectIds which reference the event objects)
Have events retain a reference to the artist (as you currently do) and then query over events using the artist's _id.
Do not store events as a separate collection and instead embed the event objects as an array inside the artist schema.
Each strategy has it's own pros and cons (read more about that here: https://docs.mongodb.com/manual/applications/data-models-relationships/)
I would think your best bet would be to go with option 2 as with option 1 you would need to make sure to have any new events or deletions of events reflected back in the artist's model as well.
In fact, since you're using mongoose you can implement what you want using virtuals (https://mongoosejs.com/docs/populate.html#populate-virtuals) as follows:
artistSchema.virtual('events', {
ref: 'Event', // The model to use
localField: '_id', // Find events where `localField`
foreignField: 'artist', // is equal to `foreignField`
justOne: false // we can have more than 1 event per artist
});
Now we only need to populate this events array, many ways to do this, one way would be:
artist.events // == undefined
artist.populate('events').execPopulate();
artist.events // == [event1, event2, etc...]
If you add this virtual to your artist object and you populate your artist object prior to your piece of code, it should execute as expected. As I mentioned though, this is just one way to achieve this.
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 using node js as my backend language. I am creating data in mongodb through submitting data using a form but I want to find the latest entered data and display it in my ejs template.
Here is my code :
/*mongoose schema setup*/
const codeSchema = new mongoose.Schema({
first: String,
second: String,
third: String,
event: String,
link: String
});
const Code = mongoose.model('Code', codeSchema);
/* GET home page. */
router.get('/welcome', async function(req, res, next) {
await Code.find({}).sort({_id: -1}).exec(function(err, data) {
if(err)
{
console.log(err);
}
else
{
//console.log(data);
res.render('index', {data: data});
}
});
});
/* GET new form form page. */
router.get('/update', function(req, res, next) {
res.render('newform');
});
/* POST update welcome page. */
router.post('/welcome', async function(req, res, next) {
const info = {
first: req.body.first,
second: req.body.second,
third: req.body.third,
event: req.body.event,
link: req.body.link
};
await Code.create(info, function(err){
if(err) {
console.log(err);
}
else {
res.redirect('/welcome');
}
});
});
The code works fine, it creates and extracts the last n record from my database but when I try to display the data in my html it does'nt shows up.I also tried using findOne() method which actually worked and displayed the last n record from my db. The problem arised when my database had no data the findOne() did'nt worked and generated error in my ejs template.
I find the data from my db using find({}) method. Like if I do First name - <%= data.first %> in my ejs template, my data does'nt show.
I also checked my mongo database which had all the information which was passed through form.
PLEASE HELP !!!!!
P.S. - I can display all the data from db using for loop but I only want to display a particular data.
Nithin K Joy here is my index.ejs
<h1>Index Page</h1>
<h4> Person1- <%= data.first %> <h4/>
<h4> Person2- <%= data.second %> <h4/>
<h4> Person3- <%= data.third %> <h4/>
<h4> Event name- <%= data.event %> <h4/>
<h4> Registration Link- <%= data.link %> <h4/>
here is my form through which recors are being submitted
<form action="/welcome" method="POST" style="width: 30%; margin: 40px auto;">
<input type="text" id="winner1" name="first" placeholder="1st">
<input type="text" id="winner2" name="second" placeholder="2nd" >
<input type="text" id="winner3" name="third" placeholder="3rd" >
<input type="text" id="event" name="event" placeholder="Event Name" >
<input type="text" id="link" name="link" placeholder="Event Regestration link" >
<button type="submit class="btn btn-success btn-block">Submit</button>
If i put my index.ejs code inside a loop it works fine but I donot want several inputs from FORM to show up in index.ejs I want only the last n record
I need help with removing this item from nested array. I tried used $http.delete but this method deleted whole ObjectID from database, and second problem is that I can't connect the click on the "removing" button with backend code.
My code:
var product = new Schema({
title: String,
price: String,
description: [ObjectID]
});
Index.html
<form name="editProduct.descriptionForm" ng-submit="editProduct.updateDescription(newDescription, editProduct.descriptionForm.description.$valid)" novalidate>
<div ng-class="{ 'has-success':(editProduct.descriptionForm.description.$valid && !editProduct.descriptionForm.description.$pristine), 'has-error':(!editProduct.descriptionForm.description.$valid && !editProduct.descriptionForm.description.$pristine) || (!descriptionForm.description.$valid && descriptionForm.$submitted) }">
<div class="entry input-group" ng-repeat="item in newDescription track by $index">
<strong><input ng-disabled="editProduct.disabled" class="form-control" type="text" name="description" ng-model="newDescription[$index]" required></strong>
<span class="input-group-btn">
<a ng-click="editProduct.deleteDescription(item);" class="btn btn-remove btn-danger">
<span class="glyphicon glyphicon-remove"></span>
</a>
</span>
</div>
</div>
<br>
<button ng-disabled="editProduct.disabled" class="btn btn-primary" type="submit">Save</button>
</form>
routes.js
router.put('/editProduct', function(req, res){
var editProduct = req.body._id;
var options = { multi: true };
Product.findOneAndUpdate({ _id: editProduct }, { $pull: { description: req.params.description }}, options, function(err, product){
if(err) throw err;
if(!product){
res.json({ success: false, message: 'Product no found' });
} else {
product.update(function(err){
if(err){
console.log(err);
} else {
res.json({ success: true, message: 'Description removed!'})
}
});
};
});
});
I also tried the following approach:
Product.findOne({'_id' : product.id}, function(err, me){
for(var i=0; i<=me.description.length; i++){
if (String(me.description[i])==String(uid)){
me.description.remove(uid);
me.save();
}
}
});
I think, the biggest problem is that I don't how to connect this function to the button.
Please try console.log(req.params.description) Before the Mongoose update query and check if the output is indeed a valid ObjectId.
If the console output is not showing the valid uid, then the problem is in the angular code. Most probably in editProduct.deleteDescription(item) function. Check if you are making Http Request by passing the correct Description Id as the parameter. Thats probably something like item.descriptionId or item.id. Debug thoroughly.
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.