Re-rendering an EJS template after jQuery POST request with new data - javascript

Update Edited my original code due to errors.
This is my first question posted here so please be gentle. :)
I am using node.js with Express and EJS forms.
What I am trying to achieve is to have an EJS template re-rendered after a jQuery POST request with new data. This is a simplified example of what I am trying to do (my original code includes a lot of fuss, SQL queries and is not properly refactored).
My goal would be to render the same template with different data when pressing a button on the page. What happening currently is when I press the button, the data is there (checking it in console), but the page does not render again with the new data. I am completely out of ideas and been stuck with this for almost a day now.
I know I could do something like
res.send(JSON.stringify(myData));
and build the HTML with a JS script, but it would be nice to be able to use EJS for better readability - my original code includes a lot of HTML elements.
Is it doable at all? Or is this an intended behaviour for rendering with Express? Sorry if I am being clueless, as I am fairly new to web development.
Anyway, here is my code, all ideas are greatly appreciated. Thanks!
test_dynamic.ejs:
<html>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<form method="post" action="/test">
<wrapper>
<h1>
<button id="toggleButton" onclick="toggle()" type="button">Toggle</button>
</h1>
<div>
<% console.log('Should show ' + name + ' with id: ' + id) %>
<li><%=name %></li>
<li><%=id %></li>
</div>
</wrapper>
</form>
</html>
<script>
var shouldShowJohn = false;
function toggle () {
var postData = {};
shouldShowJohn = !shouldShowJohn;
if (shouldShowJohn)
postData.mode = 1;
else
postData.mode = 2;
$.post('http://localhost:3000/test', postData, function (data) {
$('#toggleButton').html('').append('Showing ' + postData.mode);
});
}
</script>
routes.js:
router.get('/test', (req, res) =>{
var obj = {
name: 'DefaultName',
id: 1
}
res.render('test_dynamic', obj);
})
router.post('/test', (req, res) => {
var obj = {};
console.log('req.body.mode: ' + req.body.mode);
if (req.body.mode == 1)
obj = {
name: 'John',
id: 2
}
else
obj = {
name: 'Karl',
id: 3
}
res.render('test_dynamic', obj)
})

Please take a look at the example I've included via link below. There is a lot wrong with your HTML above, but I will focus on your question. There are several ways you can solve this problem so this is the simplest.
As stated in my comment, the workflow is:
Send POST
Respond with success/failure
If success, redirect/re-request same page from server.
Note I am not addressing EJS because when you request the page from the server that will happen naturally as you have laid out above.
Step 1: So in the sample, index.js is the server, I have basic routing to two files, index.html and page2.html. I did this so you can see the change happening. Next, in the index.html file I have a button that generates the POST request via jQuery. This takes care of step 1.
Step 2: index.js accepts the post request to /test and responds with a simple string "success" (you could respond with anything)
Step3: In index.html the $.post() handler tests for the string 'success' and the redirects to page2.html. In your scenario this would redirect back to the same page. I made it different so you could realize the change easily.
Solution
https://repl.it/#randycasburn/SimplePostResponseRedirect

Here is my solution, partially based on Randy's answer.
test_dynamic.ejs:
<html>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<body id="myBody">
<form method="post" action="/test">
<wrapper>
<h1>
<button id="toggleButton" onclick="toggle()" type="button">Toggle</button>
</h1>
<div>
<% console.log('Should show ' + name + ' with id: ' + id) %>
<li><%=name %></li>
<li><%=id %></li>
</div>
</wrapper>
</form>
</body>
</html>
<script>
var shouldShowJohn = false;
var postData = {};
postData.mode = 'toggle';
function toggle () {
$.post('http://localhost:3000/test', postData, function (data) {
$('#toggleButton').html('').append('Showing ' + postData.mode);
$('#myBody').html(data);
});
}
</script>
routes.js:
router.get('/test', (req, res) =>{
var obj = {
name: 'DefaultName',
id: 1
}
res.render('test_dynamic', obj);
})
var currentMode = 1;
router.post('/test', (req, res) => {
var obj = {};
console.log('req.body.mode: ' + req.body.mode);
if (req.body.mode == 'toggle')
if (currentMode < 3)
currentMode++;
else
currentMode = 1;
if (req.body.mode == 1)
obj = {
name: 'John',
id: 2
}
else
obj = {
name: 'Karl',
id: 3
}
res.render('test_dyanmic', obj)
})

Related

Why can't I pass functions from Express.js to EJS like I pass other variables?

I am working on a blogging application (click the link to see the GitHub repo) with Express, EJS and MongoDB.
Trying to paginate the posts I got stuck with passing newerPosts and olderPosts to the view. I am passing them just like the posts variable (and other variables):
exports.getPosts = async (req, res, next) => {
//pagination params
var perPage = 10;
var currPage = req.body.page ? parseInt(req.body.page) : 1;
const newerPosts = function() {
currPage = currPage + 1;
console.log(currPage);
}
const olderPosts = function() {
currPage = currPage - 1;
console.log(currPage);
}
const posts = await Post.find({}, (err, posts) => {
if (err) {
console.log('Error: ', err);
} else {
res.render('default/index', {
moment: moment,
layout: 'default/layout',
website_name: 'MEAN Blog',
page_heading: 'XPress News',
page_subheading: 'A MEAN Stack Blogging Application',
posts: posts,
newerPosts: newerPosts,
olderPosts: olderPosts
});
}
})
.sort({
created_at: -1
})
.skip((currPage - 1) * perPage)
.limit(perPage)
.populate('category');
};
In the view:
<div class="px-1">
<a class="btn btn-primary" href="#" onclick=<%="olderPosts()"%>>← Older Posts</a>
</div>
<div class="px-1">
<a class="btn btn-primary" href="#" onclick=<%="newerPosts()"%>>Newer Posts →</a>
</div>
Yet, I get the Uncaught ReferenceError: newerPosts is not defined error message from the browser.
What am I doing wrong? What is the closest working alternative?
You can do the following:
make a new ejs file called functions.ejs:
<script>
function test(){console.log(1)}
</script>
and then use <%- include('functions'); %> inside the template you are rendering. Use it at the start so that the function is defined when calling it.
In the corresponding route, use:
res.render('default/index', {
...,
name: 'test'
});
Inside your template can do the following:
<button onclick="<%=name%>()"> <%= btnText %> </button>
BUT, when I look at the function you want to pass, I see that you want to change the state of your application by using currPage = currPage + 1;. This wont work, since as soon as the page is rendered there is no reference to currPage in the html anymore, cause everything is static html by then.
If you want to change pages etc. you will need to implement some frontend logic.
Check this one:
const posts = await Post.find({}, (err, posts) => {
xxxx ... res.render()
})
I think here you need to make some changes in the logic of Promise usage ...
Think about - what happens if error appears ? where you handle it ?
and more interesting - after promise return result - where you exit from your function ? ...

req.body.Dates is not defined

I have 2 drop down menus that are dynamically being populated using SQL Server. Based on the selected items, I am loading a different ejs template. I have done this using the help of AJAX. However, I want to be able to load the data according to the selected criteria. For instance, if DD1 is selected as Andrew and DD2 as Date the table should load 7 columns based on those conditions.
AKA
SELECT * FROM exTable x WHERE x.Name = 'Andrew' and x.Date = '4/22/2019'
What I have already tried is to pass the selected item from the dropdown to the router, like so:
router.js
router.post('/selection', async (req, res) =>{
try {
var nameFromDB = await conn.query("SELECT DISTINCT pr.Name FROM WFS.Table1 pr WHERE pr.Group = 'Test'");
var dateFromDB = await conn.query('SELECT r.Date FROM WFS.Table2 r');
var tables = ("SELECT * FROM WFS.view v WHERE v.Date= '" + req.body.Dates + "' AND v.Name = '" + req.body.Names + "'");
console.log("SELECT * FROM WFS.view v WHERE v.Date= '" + req.body.Dates + "' AND v.Name = '" + req.body.Names + "'");
res.render('selection', {tables: tables, nameFromDB : nameFromDB , dateFromDB: datesFromDB});
}
catch (err) {
res.status(500)
res.send(err.message)
}
});
This is the output of the console.log :
SELECT top 100 * FROM WFS.view_workRequests_Extended v WHERE v.Revenue_Release_Id = '04/16/2019' AND v.Development_Manager = 'Andrew'
app.js
var bodyParser = require('body-parser');
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({
extended: true
}));
app.use('/', router);
index.ejs
<script>
$(document).ready(function() {
$('#DDD').on('change', function(event) {
var selectedDate = $('#selections option:selected').val();
});
$('#DDN').on('change', function(event) {
var selectedName = $('#selection option:selected').val();
});
$('#submitData').on('submit', function(e){
e.preventDefault();
$.ajax({
type: "POST",
url: "/selection",
data: {selectedDate : selectedDate, selectedName : selectedName },
success: function() {
alert('success');
}
});
});
});
</script>
<form action="/selection" method="POST">
<select class="DateDD" id="DDD" name="Dates">
<% for(var n=0; n < dateFromDB.recordset.length; n++) { %>
<option><%= dateFromDB.recordset[n].Date%></option>
<% } %>
</select>
<select class="NameDD" id="DDN" name="Names">
<% for(var n=0; n < nameFromDB.recordset.length; n++) { %>
<option><%= nameFromDB.recordset[n].Name%></option>
<% } %>
</select>
<input type="submit" name="Submit" id="submitData" class="btn btn-primary" value="View Report" />
</form>
selection.ejs
CONTAINS THE SAME THING AS INDEX.EJS (besides the script tag) AND ...
<table class="table table-bordered table-condensed table-striped">
<% for(var n=0; n < tables.recordset.length; n++) { %>
<tr>
<td><%=tables.recordset[n].Name%></td>
<td><%=tables.recordset[n].Date%></td>
....
....
</tr>
<% } %>
</table>
After form submit on index.ejs this error gets thrown:
Dates is not defined
I don't know whats causing this error, because I am able to see the name and date in the console being printed. Any help would be appreciated, thank you!
You've intercepted onsubmit event and modified the names of the data sent through ajax
$.ajax({
type: "POST",
url: "/selection",
data: {selectedDate : selectedDate, selectedName : selectedName }, // <-- here
success: function() {
alert('success');
}
});
So you're reading the wrong property in your req.body. You should instead read like:
// for date
req.body.selectedDate
// for name
req.body.selectedName
Also you claim
I am able to see the name and date in the console
The output of console you've put doesn't match the query in the code in question.
This is basic debugging question.
I can advice you to do couple of things like:
Check the browser devTools (network tab) to see if the body payload is being passed to the server.
On the server (express), are you using body parser middleware ? see more here (req.body section).
Try to run nodejs with inspect mode and attach your IDE (VSCode is a good one) and add some breakpoints. Alternatively, you can do some console.logs in order to check what are you getting from the client.
You can simulate a browser request using postman to check the server side and make sure it is working fine.
This is a simple error, so no big deal, frustrating as I know it can be.
I see you using dates, date, Date & Dates. I would look for a line number reference to give me a clue. I would use chrome and developer tools or console.log to see how far I get.
Or, I would use a different name convention like date1, date2, date3, date4 or something that is even more descriptive, so I knew for sure which date was what, then on narrowing down the error, it will be much easier to fix.

Making Variable Passed Through Pug Available to Javascript

I have a Pug view that gives a set of links to a user. Before you get to the page the user has already authenticated and I have the username and their department in session variables. I can pass them as variables to the view like this:
res.render('landingpage', { title: 'Landing Page',
username: req.session.username,
department: req.session.department });
And then in the view I have this line and it works:
p(class='navbar-text') Welcome #{username} from #{department}
which prints "Welcome Bob from Accounting" at the top with no problem.
But what I need to do is control whether some of the links are visible based upon the passed in department. (The department was discovered in the authentication function that passed the user onto the landing page and placed into the session.)
I was trying to place this into the document ready function but that doesn't work as it is undefined. What I need to do is to be able to change the visibility attribute and the onclick event for a link based upon the department. I have a JSON configuration file that tells me the departments allowed to access the link but I can't figure out how to translate that department variable into a javascript function that I can call to change the visibility.
I've tried to add it to a document ready function as department and #{department} but it just ends up either not knowing what it is or using it like the literal string. Any ideas of how to proceed?
Okay so I didn't post enough information for anyone to see what I was asking. Sorry. In the process of creating a new post and cutting it all down to just the part I needed I got it to work. This code works:
In the app.js file
'use strict';
var express = require('express');
var app = express();
app.set('views', './views');
app.set('view engine', 'pug');
var session = require('express-session');
var FileStore = require('session-file-store')(session);
var fileStoreOptions = {
path: './sessions',
ttl: 86400
};
var sessionOptions = {
secret: 'SecretKey',
resave: false,
saveUninitialized: false,
name: 'sessionId',
store: new FileStore(fileStoreOptions)
};
app.use(session(sessionOptions));
app.get('/landingpage', function(req,res,next) {
req.session.accessToken = true;
req.session.username = 'Bob';
req.session.department = 'Accounting';
res.render('landingpage', { title: 'Landing Page',
username: req.session.username,
department: req.session.department });
});
app.get('/images/DarkBlueIcon.png', function(req,res) {
res.sendFile(__dirname + '/images/DarkBlueIcon.png');
});
app.get('/node_modules/jquery/dist/jquery.min.js', function(req,res) {
res.sendFile(__dirname + '/node_modules/jquery/dist/jquery.min.js');
});
var server = app.listen(3000, function () { });
and this is in the pug file:
doctype html
html
head
title=title
script(src='/node_modules/jquery/dist/jquery.min.js')
body
p(class='navbar-text') Welcome #{username} from #{department}
img(id='accessApproved' src='/images/DarkBlueIcon.png' class='overlay' style='visibility: hidden;')
script.
$(document).ready( function() {
if('#{department}' === 'Accounting') {
document.getElementById('accessApproved').style.visibility = 'visible';
}
});
Gives you this:
screenshot of Hello to Bob and Blue Icon
To clarify from the other solutions: the interpolated string must be nested within quotation marks. If you don't use the quotation marks around the #{} then javascript tries to read it as a variable.
Solution:
'#{department}' === 'Accounting'
Explanation:
The solution evaluates to 'Accounting' === 'Accounting',which is true. The incorrect way would be to forget the quotes, and try #{department} === 'Accounting' which evaluates to Accounting === 'Accounting', which is the same as Undefined === 'Accounting', which is false.
You can use a hidden input to pass the department info and get the input's value in js.
Example:
doctype html
html
head
title=title
script(src='/node_modules/jquery/dist/jquery.min.js')
body
input#department(type='hidden',value=department)
p(class='navbar-text') Welcome #{username} from #{department}
img(id='accessApproved' src='/images/DarkBlueIcon.png' class='overlay' style='visibility: hidden;')
script.
$(document).ready( function() {
var department = $('#department').val();
if(department === 'Accounting') {
document.getElementById('accessApproved').style.visibility = 'visible';
}
});

http 404 error when trying to call web api controller action method from .js file

I am getting Http 404 error on button click when i inspect element in browser.
There is a wallpost.js file in Scripts folder containing logic for knockout, client side view model and data- binding etc.
In this file, reference to the WallPost Api controller is given like this---
var postApiUrl = '/api/WallPost/', commentApiUrl = '/api/Comment/';
and on my view page, there is a container for posting and commenting something like this--
<div class="publishContainer">
<textarea class="msgTextArea" id="txtMessage" data-bind="value: newMessage, jqAutoresize: {}" style="height:3em;" placeholder="what's on your mind?"></textarea>
<input type="button" data-url="/Wall/SavePost" value="Share" id="btnShare" data-bind="click: addPost"/>
now, references to script folder js files are given like this---
#section scripts{
<script src="~/Scripts/jquery.autosize.min.js"></script>
<script src="~/Scripts/knockout-3.3.0.js"></script>
<script src="~/Scripts/wallpost.js"></script>
}
First thing i want to clear that autosize.js is working fine on textarea so, i think path to wallpost.js file is correct as it is similar to autosize.js file.
Now, the problem is i am unable to post the message on button click. I have put the breakpoint at the controller's action method which should be hit on this button click, but thats not get hitted.
From what i am understanding, i think i am unable to use wallpost.js file in the Scripts folder or the route to call controller's action method is wrong So,there is a problem in reference i think.
The button click should hit the action method but it's not.
PLzz suggest me what should i try.I can provide more code if required.
I was following this article.http://techbrij.com/facebook-wall-posts-comments-knockout-aspnet-webapi
My web-api controller action method is like this----
namespace WebApp.Controllers
{
public class WallPostController : ApiController
{
private ApplicationDbContext db = new ApplicationDbContext();
public HttpResponseMessage PostPost(Post post)
{
// post.PostedBy = WebSecurity.CurrentUserId;
post.PostedBy = User.Identity.GetUserId<int>();
post.PostedDate = DateTime.UtcNow;
// post.UserProfile.UserId = WebSecurity.CurrentUserId;
ModelState.Remove("post.PostedBy");
ModelState.Remove("post.PostedDate");
// ModelState.Remove("post.UserProfile.UserId");
if (ModelState.IsValid)
{
db.Posts.Add(post);
db.SaveChanges();
// var usr = db.UserProfile.FirstOrDefault(x => x.UserId == post.PostedBy);
var usr = db.Users.FirstOrDefault(x => x.Id == post.PostedBy);
var ret = new
{
Message = post.Message,
PostedBy = post.PostedBy,
PostedByName = usr.UserName,
//PostedByAvatar = imgFolder + (String.IsNullOrEmpty(usr.AvatarExt) ? defaultAvatar : post.PostedBy + "." + post.UserProfile.AvatarExt),
PostedByAvatar = db.Users.Include(s => s.Files).SingleOrDefault(s => s.Id == post.PostedBy),
PostedDate = post.PostedDate,
PostId = post.PostId
// UserId = usr.UserId
};
HttpResponseMessage response = Request.CreateResponse(HttpStatusCode.Created, ret);
response.Headers.Location = new Uri(Url.Link("DefaultApi", new { id = post.PostId }));
return response;
}
else
{
return Request.CreateErrorResponse(HttpStatusCode.BadRequest, ModelState);
}
}
I don't know why its not working. it was working fine when i was using simple membership.now, i want to use it with aspnet identity.

Rendering collection data in a template using a reactive join with Meteor JS

I would like to output data from two collections using a reactive join into my template, then pair the users, post and comments through a common id.
So far, I can see with Mongo commands that the JSON data exist, but my template doesn't render any data. What am I doing wrong?
FYI, the meteorpad doesn't compile but the github repo will.
Repo:
https://github.com/djfrsn/frontend-interview-test-long/tree/master/ontra/ontra
Meteor Pad Example:
http://meteorpad.com/pad/SvwkNrv5grgv2uXxH/Copy%20of%20Leaderboard
There's so much wrong that it's hard to know where to start.
1) When you're loading the initial post and user data you're inserting the whole returned array as one element rather than inserting each element individually into your posts collection.
2) You're creating a publish subscription with the name "postsSet", but you're trying to subscribe to it with a different name.
3) You're not calling publishComposite correctly at all. You should be publishing the user required for each post as part of the children array.
4) The template needs updating based on the above
5) The username needs to be supplied via a helper.
6) You should really map the "id" attributes to Mongo's "_id" instead.
Here's come code which works. Note that you'll need to call meteor reset everytime you restart, otherwise you'll get duplicate id errors since you currently reimport the data every time.
Posts = new Mongo.Collection("Posts");
var groundPosts = new Ground.Collection(Posts);
Users = new Mongo.Collection("Users");
var groundUsers = new Ground.Collection(Users);
if (Meteor.isClient) {
Meteor.subscribe("postsSet");
console.log('POSTS DATA = ' + Posts.find().fetch());
console.log('USERS DATA = ' + Users.find().fetch());
Template.body.events({
"submit .ontra": function (event) {
// This function is called when the new task form is submitted
var text = event.target.text.value;
Posts.insert({
content: text,
date: new Date() // current time
});
// Clear Form
event.target.text.value = "";
// Prevent default form submit
return false
}
});
Template.body.helpers({
posts: function() {
return Posts.find();
},
});
Template.post.helpers({
username: function() {
return Users.findOne({_id: this.userId}).username;
}
});
}
Meteor.methods({
'fetchJSONData': function() {
var postsResponse = Meteor.http.call("GET","https://raw.githubusercontent.com/djfrsn/frontend-interview-test-long/master/codetest/data/posts.json");
var usersResponse = Meteor.http.call("GET","https://raw.githubusercontent.com/djfrsn/frontend-interview-test-long/master/codetest/data/users.json");
var postsData = JSON.parse(postsResponse.content);
var usersData = JSON.parse(usersResponse.content);
postsData.forEach(function (post) {
post.date = new Date();
post._id = String(post.id)
delete post.id
post.userId = String(post.userId)
Posts.insert(post);
});
usersData.forEach(function (user) {
user.date = new Date() // current time
user._id = String(user.id)
delete user.id
Users.insert(user);
});
}
});
if (Meteor.isServer) {
Meteor.publishComposite('postsSet', {
find: function () {
return Posts.find({});
},
children: [
{
find: function (post) {
console.log("%j", post.userId);
console.log("%j", Users.findOne({ _id: post.userId }));
return Users.find({ _id: post.userId });
}
}
]
});
Meteor.call("fetchJSONData");
//console.log('POSTS DATA = %j', Posts.find().fetch());
//console.log('USERS DATA = %j', Users.find().fetch());
}
HTML:
<head>
<title>ontra</title>
</head>
<body>
<div class='container'>
<header>
<h1>ontra</h1>
<form class='ontra'>
<input type='text' name='text' placeholder="Type to add new post">
</form>
</header>
<ul>
{{#each posts}}
{{> post}}
{{/each}}
</ul>
</div>
</body>
<template name='post'>
<li>
<span class="text">{{content}}</span>
<span class="text">{{username}}</span>
</li>
</template>
Your code doesn't run on meteorpad because the fetchJSONData method is executed on the server before it is defined in the common.js file. You should probably be calling the method after an event triggered on the client, or not use a method at all and simply fetch your JSON data on Meteor.startup.
Regarding the reactive join, it seems you want to do something very similar to Example 1 of the documentation: https://github.com/englue/meteor-publish-composite

Categories