Create search articles feature in MEAN stack - node.js

First of all, let me tell you that I'm a novice in the world of javascript and node.js. I have been searching for help in trying to do what i want but haven't found yet.
I am using the MEAN stack(http://mean.io/) and I am trying to implement a search feature in the included articles model. The search would look for articles with a specific tag and would be implemented in the index page. Follow me and see if you can find what I am missing please.
In the backend:
app/models/
/**
* Article Schema
*/
var ArticleSchema = new Schema({
created: {
type: Date,
default: Date.now
},
title: {
type: String,
default: '',
trim: true
},
content: {
type: String,
default: '',
trim: true
},
tag: {
type: String,
default: '',
trim: true
},
user: {
type: Schema.ObjectId,
ref: 'User'
}
});
app/controllers/
exports.searcharticle = function(req, res) {
Article.find({'tag': req.params.tag}).sort('-created').populate('user', 'name username').exec(function(err, articles) {
if (err) {
res.render('error', {
status: 500
});
} else {
res.jsonp(articles);
}
});
};
Added the route for the search in app/routes/articles.js
app.get('/articles/search/:tag', articles.searcharticle);
In the frontend:
Created the view for the search wich will display the search results - public/views/articles/search.html
<section data-ng-controller="ArticlesController" data-ng-init="searchart()">
<ul class="articles unstyled">
<li data-ng-repeat="article in articles">
<span>{{article.created | date:'medium'}}</span> /
<span>{{article.user.name}}</span>
<h2><a data-ng-href="#!/articles/{{article._id}}">{{article.name}}</a></h2>
<div>{{article.tag}}</div>
</li>
</ul>
<h1 data-ng-hide="!articles || articles.length">Your search hasn't returned any results. <br> Why don't you Create One?</h1>
</section>
The view for the index.html, where the searchbox will be implemented
<section data-ng-controller="ArticlesController">
<form role="form" data-ng-submit="searchart()">
<div>
<div>
<input type="text" id="tag" ng-model="selected" class="form-control" placeholder="Tag">
</div>
<div>
<button type="submit" class="btn btn-default">Submit</button>
</div>
</div>
</form>
Added the route to the config.js
when('/articles/search/:tag', {
templateUrl: 'views/articles/search.html'
}).
And added the search function to the articles controller
$scope.searchart = function() {
Articles.query(function(articles) {
$scope.articles = articles;
});
};
Right now, with this code implemented, when I click in the submit button in the index page, nothing happens.
Can you find what am I missing?
Thanks in advance!

In order to use a URL in your client Article Service, you should define the URL Parameter in the articles service at: packages/articles/public/services/article.js, like the articleId parameter already defined in there like this:
angular.module('mean.articles').factory('Articles', ['$resource',
function($resource) {
return $resource('articles/:articleId', {
articleId: '#_id'
}, {
update: {
method: 'PUT'
}
});
}
]);
Then you need to pass it in your angular controller search method, like the function that gets one by id, like this:
$scope.findOne = function() {
Articles.get({
articleId: $stateParams.articleId
}, function(article) {
$scope.article = article;
});
};
Personally I don't know how to add another parameter to the $resource object in addition to the existing one (articleId), you may have to create another $resource service with the new parameter (:tag) and use it in your search method in your angular controller.
Another way that sounds more simple and flexible to me is to just pass the search parameters in the query method, like this:
$scope.searchart = function() {
Articles.query({tag:$scope.selectedTag}, function(articles) {
$scope.articles = articles;
});
};
and then at the server side controller, read your query parameters like this:
exports.searcharticle = function(req, res) {
Article.find(req.query).sort('-created').populate('user', 'name username').exec(function(err, articles) {
if (err) {
res.render('error', {
status: 500
});
} else {
res.jsonp(articles);
}
});
};

Related

How to pass data from Node.js to embedded Javascript in EJS Template?

Trying to pass data fetched from our SQL server to an EJS template with some embedded Javascript. Here are some snippets of my code to show you what I am trying to do.
Route file
const data = await sqlCalls.getMenuAccess(req.user.UserIdNo)
const data2 = await sqlCalls.getAvailability();
const menus = data.recordset
const availability = data2.recordset
res.render('portal-availability',
{
title: 'Availability',
name: req.user.FirstName,
menudata: menus,
availabilitydata: availability // <--- This is the data that I want to use in my EJS template.
});
});
HTML Portion of my EJS template
<section class="content">
<div class="container-fluid">
<div class="block-header">
<h2>AVAILABILITY</h2>
</div>
<div id="grid"></div>
</div>
</section>
Javascript in my EJS template
<script>
$(document).ready(function() {
$("#grid").kendoGrid({
dataSource: {
data: availability, // <---- This is where I am trying to use availability to bind to my grid.
schema: {
model: {
fields: {
ItemIdNo: { type: "number" },
cultivardescription: { type: "string" },
containerdescription: { type: "string" },
week0: { type: "number" }
}
}
}
});
});
</script>
Any help would be greatly appreciated.
Thanks!
Jim

NodeJS/Mongoose - Combining then accessing collections

I have a field called 'allGroups' under my Blogs schema which combines all the options from 3 different models (Community, Business and Help) and allows the user to select multiple options from a dropdown list and store their ID in an array.
allGroups: [
{
type: mongoose.Schema.Types.ObjectId,
ref: "Community"
},
{
type: mongoose.Schema.Types.ObjectId,
ref: "Business"
},
{
type: mongoose.Schema.Types.ObjectId,
ref: "Help"
}
],
This is correctly saving the id of the respective option to the database; however, when I try to retrieve the name of each in my .EJS template using a forEach loop, I'm only able to return group._id, whereas I also want to return group.name (all individual models have the 'name' field). So my first question is, do I need to add something else in my model for the allGroups field to be able to access group.name, or is there another way I can access?
<% blog.allGroups.forEach((group) => { %>
<div><%- group._id %> <%- group.name %></div>
<% }) %>
Second question: As well as returning group.name, I also want to link to the respective records, which requires me to access their original models. Below is some mock code as an example; how can I reference the original model that each record belongs to so I can return the correct link prefix for each? I'm aware of Mongoose' 'instanceof' method, but I've only managed to get this to work in my controller, I'm unsure if it's possible to use within my .EJS template directly? Or is there another/better way to do this? Thanks.
<% blog.allGroups.forEach((group) => { %>
<% if (Original Model === "Business") { %>
<div><%- group.name %></div>
<% else if (Original Model === "Help") { %>
<div><%- group.name %></div>
<% else if (Original Model === "Community") { %>
<div><%- group.name %></div>
<% } %>
<% }) %>
Current controller for reference:
async blogShow (req, res, next) {
let blog = await blog.findById(req.params.id).populate('interviewees allGroups likes').populate({
path: "comments",
model: "Comment",
options: { sort: { createdAt: -1 }}
});
res.render("blogViews/showBlog", { blog, cloudinary });
},
You have to render the ejs file here res.render("blogViews/showKnowPangyao" it should be something like blog.ejs or blog:the name of the ejs file, and in place of knowPangyaoyou have to pass the results returned by query that is blog not {knowPangyao, cloudinary } it should be { blog:blog }.
TRY:
async blogShow (req, res, next) {
let blog = await blog.findById(req.params.id).populate('interviewees allGroups likes').populate({
path: "comments",
model: "Comment",
options: { sort: { createdAt: -1 }}
});
res.render("blogViews.ejs", { blog:blog });
},

Accessing child methods in parent for mongoose succeeds in array and fails with single child

UPDATE : Solution is at bottom of question
I have an express site using mongoose.
I'll greatly simplify to say that I have adults, kids, and house models. When I create methods on kids, I can call them from within methods on adults and get a result. I can also call them from my .ejs views. However, when I create methods on house, I can only get a result from my .ejs views and get undefined when called from within methods on adults. Example code follows.
adult.js
const mongoose = require('mongoose');
const adultSchema = mongoose.Schema({
name: { type: String },
size: {type: String},
kids: [{type: mongoose.Schema.Types.ObjectId, ref: 'Kid', required: true}]
house:{type: mongoose.Schema.Types.ObjectId, ref: 'House', required: true}
});
adultSchema.method({
getKidsDescription: function() {
if (this.kids.length < 1) {
return 'No kids yet';
} else {
let ev = 'Kids, aged: ';
let kds = this.kids;
kds.forEach(function(k){
ev = ev + 'k.getAge()' // works
})
return ev;
}
},
getHouseDescription: function(){
return 'A fabulous house on '+this.house.getFullStreet(); // does not work
}
})
module.exports = mongoose.model('Adult', adultSchema);
kid.js
const mongoose = require('mongoose');
const kidSchema = mongoose.Schema({
name: { type: String },
size: {type: String},
birthdate: {type:Date}
});
kidSchema.method({
getAge: function() {
return (Math.floor(new Date() - this.birthdate)/(1000*60*60*24*365))
},
})
module.exports = mongoose.model('Kid', kidSchema);
house.js
const mongoose = require('mongoose');
const houseSchema = mongoose.Schema({
name: { type: String },
city: {type: String},
street: {type:String}
});
houseSchema.method({
getFullStreet: function() {
return this.street + ' Road';
},
})
module.exports = mongoose.model('House', houseSchema);
When I make a query for theAdult, it looks like this:
controller.js
exports.main = async (req, res, next) => {
if (req.theAdult) {
try {
const found = await db.fetchAdult(req.theAdult._id)
res.render('/main', {
//theHouse: found.house //below I show this working
});
} catch(e) {
throw new Error(e.message)
}
} else {
res.redirect('/');
}
}
db.js
exports.fetchAdult = (id) => {
return Adult.findById(id)
.populate({ path: 'kids'})
.populate({ path: 'house'})
.exec()
.then(doc => {
return doc;
});
}
Assuming house is passed to view as an object when rendered (commented out above), this works
view.ejs
<p> <%= theHouse.getFullStreet() %></p>
Assuming house populated on the call to load the Adult, this returns undefined.
view.ejs
<p> <%= theAdult.house.getFullStreet() %></p>
At the same time, both of these work
view.ejs
<ul> <% theAdult.kids.forEach(function(k) { %>
<li><%= k.getAge() %> </li>
<% }); %>
</ul>
<p> <% theAdult.getKidsDescription() %> </p>
What I am not understanding is how the method calls work for objects in array and work in the view but do not work for objects on in an array. This is a single child error (for me). If it did not work in the view, I would assume that the method getFullStreet() was the problem, but it works in the view. If the array methods could not be called within the parent, I would assume the issue was with trying to access getFullStreet() in the parent.
What am I missing?
SOLUTION
I was fetching theAdult in my call to show view.ejs, but I was then actually relying on currentAdult which referred to req.adult and did not have the fields populated. My solution was to add a pre hook to the adult schema that always populates house on find.
in adult.js
adultSchema.pre('find', function() {
this.populate('house')
})
Have you tried passing a hydrated theAdult? It might only see the ObjectID, without any other data or methods.

How do I update a form that has multiple values in ejs + mongoose?

a user has fields in mongoose which will get updated if the user decided to update.
Here's the user schema
var User = Schema({
education: [{ type: String}],
});
So basically a user has fields that they could update or add, for example a user can add additional education and skills information using a form.
How do I properly do it in ejs and route?
my attempt in the route.js
router.post('/update-resume', function(req, res) {
User.findById(req.user._id, function(err, foundUser) {
// This part how do I update ?
if (req.body.education) foundUser.resume.education.push(req.body.education);
foundUser.save();
});
});
The value keeps pushing, i want to , I know that it is obvious that I'm pushing the data to the field, but how do I update it properly?
Form.ejs
<div class="form-group">
<label for="education">Education:</label>
<% for(var i = 0; i < user.resume.education.length; i++) { %>
<input type="text" class="form-control" name="education" id="education" value="<%= user.resume.education[i] %>">
<% } %>
</div>
Is it true that I need to for loop each field? if I want to update the specific data?
1st option:
Depending on how you use the application, you might not even care about updating - you could just delete the previous educations and save new ones instead.
2nd option:
To properly update you really need some kind of an ID that you can refer to when updating, right?
You'll still need to use your for loop, you just need to insert the id hidden field.
To do that you will need to pass an object and not an only-string value. I have made my object array look like this:
var education = [
{content:'Education 1',id:1},
{content:'Education 2',id:3},
{content:'Education 3',id:5},
{content:'Education 4',id:2},
];
Then you can do something like that:
<% for(var i = 0; i < education.length; i++) { %>
<input type="hidden" type="hidden" name="education_id" value="<%= education[i].id %>"/>
<input type="text" class="form-control" name="education" id="education" value="<%= education[i].content %>">
<% } %>
Array will always get passed the way you have sent it to the server. In my case, I'll get this (you can see that everything's in the order that it should be):
{education_id: [ '1', '3', '5', '2' ],
education: [ 'Education 1', 'Education 2', 'Education 3', 'Education 4' ] }
Now, let's look at the POST backend, you will need to tie everything back to an object (you don't really need to, but you can and probably should for the sake of sanity):
var education_i;
var education_req = [];
for(education_i=0;education_i<req.body.education.length;education_i++) {
console.log(req.body.education[education_i]);
education_req.push({
content:req.body.education[education_i],
id:req.body.education_id[education_i]
});
}
A few more notes:
You HAVE TO check for the user of every education record. You don't want to let anyone mess with the IDs and ruin someone else's profile.
You should probably save the array length variable separately, outside of the loops as it might cause performance issues on very large arrays because the length is parsed on every loop iteration.
This is the way I would do it:
The model:
var User = new Schema({
education: [{ content: String }]
});
The EJS/HTML:
<form id="education">
<% user.education.forEach(function(item) { %>
<input type="text" data-id="<%= item.id %>" value="<%= item.content %>" />
<% }); %>
<button type="submit">Save</button>
</form>
The client side javascript (JQuery):
$('#education').on('submit', function(e) {
e.preventDefault();
var data = [];
$(this)
.find('input')
.each(function() {
var $this = $(this);
// Collect the data with the id and value
data.push({
id: $this.data('id'),
value: $this.val()
});
});
$.ajax({
url: '/update-resume',
type: 'post',
data: { data: JSON.stringify(data) }
})
.done(function(data) {
if (data.success) {
// Lazy: refresh window
window.location.reload();
}
})
.fail(function() {
// Show an error or something fancy
});
});
The above javascript will read the data-ids from the input and the values and put them into an array of education objects. It will then stringify the object add and the string to the key 'data'. This means that you can pull the string in the route from req.body.data and parse it.
The server side javascript/in the route:
router.post('/update-resume', function(req, res, next) {
User.findById(req.user._id, function(err, user) {
var parsed = JSON.parse(req.body.data);
// update and remove
var results = user
.education
.filter(function(item) {
return parsed.some(function(input) {
return input.id === item.id;
});
})
.map(function(item) {
var related = getRelated(item.id, parsed);
return { content: related.value };
});
// Add new items
user.education = results
.concat(parsed.reduce(function(prev, curr) {
if (!getRelated(curr.id, results)) {
prev.push({ content: curr.value });
}
return prev;
}, []));
user.save(function(err) {
if (err) return next(err);
res.json({ success: true });
});
});
});
Get related helper:
var getRelated = function(id, arr) {
for (var i = 0; i < arr.length; i++) {
if (String(arr[i].id) === String(id)) return arr[i];
}
};
Mongoose will automatically give your education array items an id. The above will allow you to be able to add, remove and update existing education items on the page.

Why is 'encryptedPassword' parameter being interpreted as undefined in sails.js project?

I am working on a web app following irlnathan/ponzicoder's Sailscasts series as a guide. I have reached Ep. 5 on creating entries for the 'User' model using a signup action. At this stage in the tutorial (precise time linked to), we see that the response page he receives is the JSON object of the User that was just created using the signup. I, however, get the following for my response page:
{
"error": "E_VALIDATION",
"status": 400,
"summary": "1 attribute is invalid",
"model": "User",
"invalidAttributes": {
"encryptedPassword": [
{
"rule": "string",
"message": "`undefined` should be a string (instead of \"null\", which is a object)"
},
{
"rule": "required",
"message": "\"required\" validation rule failed for input: null"
}
]
}
}
This states that my 'encryptedPassword' parameter is somehow undefined by the time I am returning a response. However, when I log the json object I am returning to the response in my console, I get the following (which is what I expect to see instead of the E_VALIDATION response):
{ firstName: 'First',
lastName: 'Last',
email: 'email#domain.com',
encryptedPassword: '$2a$10$q0TfKWnN47mucsZLybN8WeygPPMpYxp9VMGUgIjA./ipZBn.POuOG',
lastLoggedIn: '2015-08-03T23:45:22.520Z',
gravatarUrl: 'http://www.gravatar.com/avatar/7328fddefd53de471baeb6e2b764f78a?',
createdAt: '2015-08-03T23:45:22.523Z',
updatedAt: '2015-08-03T23:45:22.523Z',
id: 39 }
So encryptedPassword is definitely being correctly created and defined.
This is a problem similar to the one faced by THIS user, except my problem does not lie in missing HTML name attributes. Stranger still, my problem specifically lies in the 'encryptedPassword' parameter and none of the others.
I must admit that since I am not using the exact same fields for my signup form as the tutorial, there may be some discrepancies which are causing the issue. However, I still haven't been able to figure it out after a few hours of debugging. Here's what I'm using
Relevant code:
Signup action (UserController.js <-- following sails.js conventions)
And as a note for each of the console.log statements littered in the signup function, I get a non-null value for them all. I did this to verify that indeed encryptedPassword was not null/undefined
/**
* UserController
*
* #description :: Server-side logic for managing Users
* #help :: See http://sailsjs.org/#!/documentation/concepts/Controllers
*/
module.exports = {
signup: function(req, res) {
var Passwords = require('machinepack-passwords');
// Encrypt a string using the BCrypt algorithm.
Passwords.encryptPassword({
password: req.param('password')
// difficulty: 10,
}).exec({
// An unexpected error occurred.
error: function (err){
return res.negotiate(err);
},
// OK.
success: function (encryptedPassword){
console.log("1", encryptedPassword);
require('machinepack-gravatar').getImageUrl({
emailAddress: req.param('email')
}).exec({
error: function(err) {
return res.negotiate(err);
},
success: function(gravatarUrl) {
console.log("2", encryptedPassword);
console.log("2", gravatarUrl);
User.create({
firstName: req.param('firstName'),
lastName: req.param('lastName'),
email: req.param('email'),
encryptedPassword: encryptedPassword,
lastLoggedIn: new Date(),
gravatarUrl: gravatarUrl
}, function userCreated(err, newUser) {
console.log("NEWUSER", newUser);
console.log("3", encryptedPassword);
console.log("3", gravatarUrl);
if (err) {
console.log("err: ", err);
console.log("err.invalidAttributes: ", err.invalidAttributes)
// If this is a uniqueness error about the email attribute,
// send back an easily parseable status code.
if (err.invalidAttributes && err.invalidAttributes.email && err.invalidAttributes.email[0]
&& err.invalidAttributes.email[0].rule === 'unique') {
return res.emailAddressInUse(); //some error that I have already defined somewhere else in my project
}
return res.negotiate(err);
}
return res.json({
id: newUser.id
});
});
}
});
}
});
}
};
Attributes of the User model (User.js <-- following sails.js conventions)
/**
* User.js
*
* #description :: TODO: You might write a short summary of how this model works and what it represents here.
* #docs :: http://sailsjs.org/#!documentation/models
*/
module.exports = {
schema: true,
attributes: {
//all the other attributes
// The encrypted password for the user
encryptedPassword: {
type: 'string',
required: true
}
//more attributes
}
};
HTML of the signup form
<form ng-submit="submitSignupForm()" id="sign-up-form" name="signup" class="form" role="form">
<!-- irrelevant html -->
<!-- P A S S W O R D -->
<div class="control-group form-group" ng-class="{'has-error':signup.password.$invalid && signup.password.$dirty}">
<input type="password" name="password" value="" class="form-control input-lg" placeholder="Password" ng-model="signupForm.password" id="password" required ng-minlength="12"/>
<span class="help-block has-error" ng-if="signup.password.$dirty">
<span ng-show="signup.password.$error.required">Password is required.</span>
<span ng-show="signup.password.$error.minlength">Password must be at least 12 characters.</span>
</span>
</div>
<!-- more misc html -->
</form>
And the signup controller
angular.module('SignupModule').controller('SignupController', ['$scope', '$http', 'toastr', function($scope, $http, toastr) {
$scope.signupForm = {
loading: false
}
$scope.submitSignupForm = function() {
$scope.signupForm.loading = true;
console.log("clicked!");
$http.post('/signup', {
firstName: $scope.signupForm.firstName,
lastName: $scope.signupForm.lastName,
email: $scope.signupForm.email,
password: $scope.signupForm.password
})
.then(function onSuccess() {
window.location = '/user';
})
.catch(function onError(sailsResponse) {
var emailAddressAlreadyInUse = (sailsResponse.status == 409);
if (emailAddressAlreadyInUse) {
toastr.error('That email address has already been taken, please try again.', 'Error');
return;
}
})
.finally(function eitherWay() {
$scope.signupForm.loading = false;
});
}
}]);
So to reiterate, why on earth is encryptedPassword being interpreted as null?

Resources