I'm figuring out what's the best way to update a current list of results from an API call, with a new list of results from an API call.
I'm making API request to news API and loading them into the index page when it first loads:
app.get("/", function (req, res) {
request("https://newsapi.org/v2/top-headlines?q=" + initialQ + "&category=sports&pageSize=10&page=" + page + "&sortBy=relevance&apiKey=" + apiKey, function (error, response, body) {
if (!error && response.statusCode == 200) {
let data = JSON.parse(body);
totalResults = data.totalResults;
console.log(totalResults)
let articles = scripts.articlesArr(data);
let filteredArticles = scripts.filteredArr(articles);
res.render("index", { filtered: filteredArticles });
} else {
res.redirect("/");
console.log(response.body);
}
});
});
Then the user will toggle two buttons to get more results, or go back a page:
app.post("/", function (req, res) {
let inputValue = req.body.page;
let pages = Math.ceil(totalResults / 10)
page = scripts.iteratePages(inputValue, page, pages);
request("https://newsapi.org/v2/top-headlines?q=" + initialQ + "&category=sports&pageSize=10&page=" + page + "&sortBy=relevance&apiKey=" + apiKey, function (error, response, body) {
if (!error && response.statusCode == 200) {
let data = JSON.parse(body);
let articles = scripts.articlesArr(data);
let filteredArticles = scripts.filteredArr(articles);
res.render("index", { filtered: filteredArticles });
} else {
res.redirect("/");
console.log(response.body);
}
});
});
I'm aware of Socket io, but I was wondering if there are other means or methods of achieving this? From what I understand, I can update frontend content via the front end - but with my current set up I'd much prefer to update from the back end
EJS code:
<div id="container">
<% for(var i=0; i < filtered.length; i++) { %>
<ul>
<li><%= filtered[i].title %></li>
<li><%= filtered[i].date %></li>
<li><img src="<%= filtered[i].image%>" /></li>
<li><%=filtered[i].description%></li>
<li><%= filtered[i].link %></li>
</ul>
<% } %>
</div>
<form action="/" method="POST">
<ul>
<li>
<button type="submit" name="page" value="next">Get more results</button>
<button type="submit" name="page" value="prev">Go back a page</button>
</li>
</ul>
</form>
For bi-directional communication we can use WebSockets (with a library like Socket.IO), for uni-directional server-to-client we can use EventSource, and for uni-directional client-to-server we use good ol' HTTP, through fetch or XMLHttpRequest in the browser API (this is referred to as AJAX, though I think most devs just says "client calls the server" these days). For 99% of use cases what we want is client-to-server over HTTP. If I understand correctly then you want stuff to happen when the users pushes a button. That's a case of client-to-server.
User pushes button
Client calls our new API endpoint /articles with fetch to get more articles: const data = await fetch('localhost:8080/articles'); const articles = await data.json(). A simplified version of the code for /articles looks something like app.get('/articles', (req, res) => request("https://newsapi.org").then(articles => /* do stuff with articles here */res.send(result)). This end point returns json instead of html (which our / endpoint returns)
Our server calls newsapi. Newsapi anserrs our server. Our server answers the client.
Then we need some data binding/templating that ensures that the DOM is updated with the new articles. This is functionality that libs like React and Angular supply. But for learning purposes and to keep things simple you can do something like articles.forEach(a => {const el = document.createElement('li'); el.innerHtml = a; document.getElementById('articles').appendChild(el)}), assuming a tag <ul id="articles">... where articles are supposed to show up exists (you probably want to do something more complex with your articles, but you get the idea)
Page hasn't reloaded 🙌
Update: some code review :)
use template literals. "https://newsapi.org/v2/top-headlines?q=" + initialQ + "&category=sports&pageSize=10&page=" + page + "&sortBy=relevance&apiKey=" + apiKey -> https://newsapi.org/v2/top-headlines?q=${initialQ}&category=sports&pageSize=10&page=${page}&sortBy=relevance&apiKey=${apiKey}
Prefer const over let
Use new lines when you're lines get very long (many go by 80 columns as preferred max width)
It looks like you do one ul for each article and one li for each property on the article. ul is a list (unordered list) and li is a list item. So one ul should contain many li, and each li should contain one item (in this case an article). You can read more about semantics in web development here
Related
Have just been playing around with axios & cheerio. I was attempting to scrape table data from the world rugby rankings website. I would like to return the top 10 rows of the table.
https://www.world.rugby/tournaments/rankings/mru
Presently I can only retrieve the first row of the table and I can't figure out why.
const axios = require('axios')
const cheerio = require('cheerio')
async function getWorldRankings() {
try {
const siteUrl = 'https://www.world.rugby/tournaments/rankings/mru'
const { data } = await axios({
method: "GET",
url: siteUrl,
})
const $ = cheerio.load(data)
const elemSelector = 'body > section > div.pageContent.flex-content > div:nth-child(2) > div.column.large-8 > div > section > section > div.fullRankingsContainer.large-7.columns > div > div > table > tbody > tr'
$(elemSelector).each((parentIndex, parentElem) => {
$(parentElem).children().each((childIndex, childElem) => {
console.log($(childElem).text());
})
})
} catch (err) {
console.error(err);
}
}
getWorldRankings()
Result:
>node index.jsx
Position
Teams
Points
For full context and credit I was playing around with this guide:
https://www.youtube.com/watch?v=5YCuUCRS_Ks (I'm using the same code just different url's and css selectors - and I can retrieve table rows as intended with his example coinmarketcap.com and many other websites).
For the world rugby rankings site - even though the html is available in dev tools is the data being injected in some way that makes it unselectable? (I have no idea what I am talking about just throwing out a guess).
Thanks for any help.
node v16.4.2
"axios": "^0.22.0",
"cheerio": "^1.0.0-rc.10",
The data for that table is being loaded later with AJAX and is not initially loaded with the page, you therefore cannot select it with Cheerio. The good news is you don't even need Cheerio for this. If you take a look at the network requests tab in your browser's development tools, you'll see the AJAX request being made uses the following URL to load JSON formatted data --the data you want-- into the page:
https://cmsapi.pulselive.com/rugby/rankings/mru?language=en&client=pulse
I want to make a system that limits the number of posts that get displayed and a load more button that loads them from where the limit stoped previously WITH the capability to change the ordering of those same posts.
Right now I have:
html/ejs:
<main>
<div class="container">
<%
if(results.lenght != 0){
var i = 1;
results.forEach(function(results){
%>
<div class="post">
<div class="op"><%= results.username %></div>
<h2 class="post-title"><%= results.title %></h2>
<div class="content"><p class="post-content"><%= results.content %></p></div>
</div>
<% i++; }) %>
<% } else { %>
locals.message1 = 'No posts found :(';
<% } %>
</div>
<div class="load-container"><a class="load" href="">Load more</a></div>
</main>
dropdown to select sorting:
<div class="drop">
<button onclick="drop()" class="drop-btn">Sort by</button>
<form id="dropdown-content" class="dropdown-content" method="POST">
<button type="submit" formaction="/" value="1">Newest</button>
<button type="submit" formaction="/" value="2">Oldest</button>
<button type="submit" formaction="/" value="3">Popular</button>
</form>
</div>
routes:
router.get('/', authController.isLoggedIn, (req, res, next) => {
sql = 'SELECT posts.username, time, title, content, user_file, audio FROM posts JOIN user on posts.user_id = user.id';
db.query(sql, function(err, results, fields){
if(err) throw err;
else if(results.length!=0){
res.render('index', {
user: results: results, time: moment.utc(new Date(results.time)).fromNow()
});
}
else {
res.render('index', {
user: message: 'Sorry, we don\'t have any posts :(', message1: 'Very sad...', results: results
});
}
})
});
I was thinking of having two variables in the route that would store the limit. Sort of like this:
var limit1: 0; // where to start displaying posts
var limit2: 8; // how many to display before stopping
// ADD 8 to both when button is clicked
var limit = 'LIMIT ' + limit1 + ', ' + limit2 + ';' //combining it for LIMIT in MySQL
db.query(sql + limit, function(err, results, fields)...
The problem is that I don't know how to keep track of how many I've already loaded and how to pass that data from the load more button in the EJS file to the router. The only way I currently know how to do is with a post form but I'm guessing that wouldn't be good at all.
For the sorting I would want to do basically the same thing:
// when new sorting is selected
var sorting = sorting; //selecting a sorting algorithm based on the button clicked
// reset the limits when new sorting is selected
limit1 = 0; limit 2 = 8;
db.query(sql + sorting + limit, function(err, results, fields)...
But again I don't know how I would tell the route about the changes and how I would store the limits... I've tried some things but I couldn't get the variable from the EJS on button click to the route('/'... So if I only got one thing out of this I would want it to be the variable passing.
I'm assuming you are brining back some results with the page load first correct?
With that assumption you have your main loop to display the posts
<main>
<div class="container">
<%
if(results.lenght !== 0){
results.forEach(results =>{
%>
<div class="post">
<div class="op"><%= results.username %></div>
<h2 class="post-title"><%= results.title %></h2>
<div class="content"><p class="post-content"><%= results.content%>
</p></div>
</div>
<%}) %>
<% } else { %>
No posts found :(
<% } %>
</div>
<input type="hidden" id="postCount" value="<%=results.length%>">
<div class="load-container"><a class="load" href="">Load more</a></div>
</main>
(slight edits made to use arrow function, also not sure what the count was for? But its not really needed from what I can tell, if you needed the count you could always just use the length of the results as they would be the same.)
Now, you need an api route of some kind to pass the update variables to so you can make the request. It will be similar to the original route, so in the same route file you would have something like
router.get('/update/:sort/:start', authController.isLoggedIn, (req, res, next) => {
// place relevent SQL code here that uses the passed params
// then send back JSON object back to the browser
res.json(jsonresponse)
});
So, in this sample, you would be passing the sort and start argument as part of an XHR/Fetch request, as you can see above, we have placed a hidden variable with the length of the original request so we know how many were returned, we will pass that as the 'start' value to the update endpoint and tell SQL to start at that record
(Note, you could extend that route to have other limits passed etc, Also doing dynamic queries like this can open you up for a SQLinjection so you will need to sanities those before passing them to the SQL Query)
From here you will have a change event handler on your page, that will listen for change events on the dropdown, from there you will take the value of that and pass that to an XHR request that hits the api endpoint with the passed variables, ones it returns the JSON object you will simply loop over it and append to the screen, once the loop is done, grab the total from the hidden object and add the total from the returned object so the next request knows where you left off (more for the load more option assuming)
I am creating a to-do list and need help with adding a button in EJS that can delete an object from an array itemArr. I am displaying my <li> items via a for loop, which are objects in the array. Each object has a name, start time, end time and an id.
itemArr.push({name:newItem, st:starttime, et:endtime, id:id});
Is there a way to create an onclick button that can delete an object from the array and hence remove it from the "To do" listing? I can easily remove it in html, but when i add a new item, the array is refreshed and the listing will reappear, so i must remove it from the array.
Currently, this is my EJS Code, where the List is generated from a for loop, and res.render shows where the itemArr is binded to ITEMARRAY.
<body>
<h2 style="text-decoration: underline;"> To do:</h2>
<ol type="1">
<% for (var i=0;i<ITEMARRAY.length;i++){ %>
<li> <%= ITEMARRAY[i].name %> | <%= ITEMARRAY[i].st %> - <%= ITEMARRAY[i].et %> <input type="checkbox"> <button onclick="myFunction(i)" id="remove">X</button> </li>
<% } %>
</ol>
<script>
function myFunction() {
var x = document.getElementById("remove").parentNode.remove();
}
</script>
Here is the app.js:
app.get("/", function (req, res) {
res.render("list", {ITEMARRAY: itemArr}); //pass an object into list.ejs with keyval pair
});
app.post("/", function(req,res){
var newItem = req.body.newItem;
var starttime = req.body.startTime;
var endtime = req.body.endTime;
var id = 0;
//checking validity of range
var start = parseInt(starttime.split(":"));
var end = parseInt(endtime.split(":"));
var difference = (end - start) / (86400000 * 7);
if (difference < 0) {
// throw new Error("The start time must come before the end time.");
res.render("fail");
}
else
itemArr.push({name:newItem, st:starttime, et:endtime, id:id}); //if all ok, then push to arr
sortList(itemArr);
res.redirect("/");
})
TL; DR; EJS is a server-side rendering solution so you cannot do that.
Because EJS is an SSR solution all the rendering happens on the server and then the client receives an HTML page. From now the page lives in the Browser, and to change it you need to use some browser JS code to do that.
the answer is to use mongodb, then make a post request via the button.
Alright so I can't figure out how to explain this. Basically I am writing a simple express app that requests the omdb api to search for a movie title and return the results. Now the omdb api send back the results as pages with 10 results per page.
This is my app.js file
let express = require("express");
let app = express();
let request = require("request");
app.set("view engine", "ejs");
let query = "";
let page = 1;
app.get("/", (req, res) => {
res.render("search");
});
app.get("/results", (req, res) => {
if(req.query.search){
query = req.query.search;
}
if(req.query.page){
page = req.query.page;
}else{
page = 1;
}
console.log(query);
console.log(page);
let url = "http://omdbapi.com/?apikey=thewdb&s=" + query + "&page=" + page;
request(url, (error, response, body) => {
if(!error && response.statusCode == 200){
let data = JSON.parse(body);
res.render("results", {data: data});
}
});
});
app.listen(process.env.PORT, process.env.IP, () => {
console.log("Server Started");
});
This is my search.ejs file
<h1>Search for a Movie</h1>
<form action="/results" method="GET">
<input type="text" placeholder="search term" name="search">
<input type="submit">
</form>
And this is my results.ejs file
<h1>Results of Search</h1>
<ul>
<% data["Search"].forEach((movie) => { %>
<li>
<strong><%= movie["Title"] %></strong> - <%= movie["Year"] %>
</li>
<% }); %>
</ul>
<form action="/results" method=GET>
<input type="text" name="page" placeholder="Page">
<input type="submit">
</form>
Search Again
This works where the user can type the page number in the results page and go to the next page. However, what happens is when I send back the page number, the search query is lost. Right now my solution is to define a global variable and keep track of the query like that, but is there a proper way to do this?
I have recently started learning backend development so I am very new to express and node.
Right now my solution is to define a global variable and keep track of the query like that
Don't do that.
You'll get race conditions. And cross-user pollution.
Just put the query in the form.
<input type="hidden" name="query" value="...">
In my code below I have created an array of items in my .JS file. I was then able to pass this array to the .Jade and use each value in the array as an item in a dropdown list. I now want to pass the user input of which item they will click in the dropdown back to the server side (.js) so that I can use the user input to find more data.
My problem is that I don't know how to send the .jade variables to the server side. I want to send the "this.selectedIndex"/selected "val" so I can use it as a variable in the javascript file.
.JS
router.get('/', function(req, res) {
var projectPathArray = [];
async function main() {
var projects = await _db.listProjects();
projects.forEach(async (item) => {
var pathy = item.path;
projectPathArray.push(pathy)
})
res.render('index', { title: 'Projects', projectPathArray:projectPathArray});
}
main();
.jade
extends layout
script(src="libs/jquery-1.11.3.min.js")
link(rel='stylesheet', href='/stylesheets/style.css')
block content
h1= title
p To start, please select a project
html
body
form#test-form(action='', method='get')
select#menu1(name='menu1', size=projectPathArray.length)
each val in projectPathArray
option=val
Without understanding exactly what you want this should at least get you closer to what you are asking for.
1) Add the route to handle the post where you can retrieve the values posted back in the form using req.body.
2) In your Pug/Jade template I indented the form elements so they are under the form, added a submit button, and changed the method of the form to post.
.JS
router.post('/', function(req, res) {
console.log(req.body);
res.redirect('/');
});
router.get('/', function(req, res) {
var projectPathArray = [];
async function main() {
var projects = await _db.listProjects();
projects.forEach(async (item) => {
var pathy = item.path;
projectPathArray.push(pathy)
})
res.render('index', { title: 'Projects', projectPathArray:projectPathArray});
});
main();
.jade
extends layout
script(src="libs/jquery-1.11.3.min.js")
link(rel='stylesheet', href='/stylesheets/style.css')
block content
h1= title
p To start, please select a project
html
body
form#test-form(action='', method='post')
^
select#menu1(name='menu1', size=projectPathArray.length)
each val in projectPathArray
option=val
button(type='submit') Submit
You will need to use some mechanism for communicating from the frontend back to the server. This includes, but is not limited to, websockets and/or AJAX.