Setting/Accessing Nunjucks global variables within a NodeJS/KeystoneJS project - node.js

I've spent all of this morning searching for how to do this, and have come up stumped.
The project I'm working on is built on KeystoneJS/NodeJS. It's using Nunjucks which I've only got a few days basic experience of.
My issue is that after loading the config vars that sets the URI's/Ports of the services, I then want to set these up as Nunjucks variables, so within the html views, I can use those as the src locations.
I can't share all the code here, as I'm working on a government (UK) project but here's enough I hope.
Keystone.js
// Require keystone
var keystone = require('keystone');
var cons = require('consolidate');
var nunjucks = require('nunjucks');
var env = new nunjucks.Environment(null);
env.addGlobal('provision_uri', 3);
This loads initially, after routing it calls:
Login.js
var keystone = require('keystone');
var nunjucks = require('nunjucks');
exports = module.exports = function (req, res) {
var view = new keystone.View(req, res);
var locals = res.locals;
// locals.section is used to set the currently selected
// item in the header navigation.
locals.section = 'home';
var env = new nunjucks.Environment(null);
var provision_uri = env.getGlobal('provision_uri',3);
console.log(`Uri ${provision_uri}`); **<-- ERRORS HERE**
// Render the view
view.render('login', {
title: 'User Login',
description: 'Login to your Account',
provision_uri: provision_uri
});
};
Login.html
<br>
<form action="{{provision_uri}}/session" method="post" name="user">
<div class="container">
When I then start the project, the landing page loads, click on the login page and within console I get:
GET /provision/ 304 74.147 ms
Error thrown for request: /login
Error: global not found: provision_uri
I've checked this Question however it doesn't answer what I need but I looked up the environment.addGlobal given as an answer. That did seem to be what I wanted, but still it wouldn't work. I found this question which provided hope.
Any ideas would be great, I do have a work-around but would like to learn how to use these.
Thanks,
Colin

You don't need to create new instance of nunjucks environment on each render call. The new scope (environment) has empty global space.
var nunjucks = require('nunjucks');
var env = new nunjucks.Environment(null);
env.addGlobal('provision_uri', 3);
...
exports = module.exports = function (req, res) {
...
var provision_uri = env.getGlobal('provision_uri',3);
console.log(provision_uri);
view.render('login', {
title: 'User Login',
description: 'Login to your Account',
// provision_uri: provision_uri // it's not necessary
});
}

Related

How do you send an object to a helper in Handlebars?

Learning handlebars and Express I'm trying to learn of a way to send an object without always having to build in the render. For example if I have:
const details = {
version: process.env.npm_package_version,
author: 'foobar'
}
I can send this to my footer.hbs in partials from:
app.get('/', (req, res) => {
res.render('index', {
details
})
})
but looking for a way to send it to a template file and not always in a render I've read in the documentation about block helpers and tried:
// Define paths for Express config
const publicDir = path.join(__dirname, '../public')
const viewsPath = path.join(__dirname, '../templates/views')
const partialsPath = path.join(__dirname, '../templates/partials')
// Setup hbs engine and views location
app.set('view engine', 'hbs')
app.set('views', viewsPath)
hbs.registerPartials(partialsPath)
hbs.registerHelper('appDetails', () => {
const details = {
version: process.env.npm_package_version,
author: 'foobar'
}
return details
})
but in my directory /partials from file footer.hbs I try to use the helper:
<footer>
<p>Created by {{details.author}} | version: {{details.version}}</p>
</footer>
and it doesn't work. I've searched the site and I've read:
How to set a variable for the main handlebars layout without passing it to every route?
nodejs + HBS (handlebars) : passing data to partials
Passing variables through handlebars partial
In my Node and Express app is there a way to send data to the partials file without having to always send it in render?
There are two ways a Handlebars Helper can add data to the rendering context of a template:
1) By directly mutating the template's context or 2) By using private variables
Example: https://codepen.io/weft_digital/pen/JjPZwvQ
The following helper updates or adds the data points name and newData to the template's global context, and it also passes a private variable, using the data option.
Handlebars.registerHelper('datainjection', function(context, options) {
this.name = "Bob";
this.newData = "Updated"
return context.fn(this, { data: {private:"pirate"} });
});
Before you call the {{#datainjection}}{{/datainjection}} block in your template, {{name}} will be set to whatever value you pass to the render function, but every occurrence of {{name}} within or after the {{#datainjection}}{{/datainjection}} block will use the updated value, which is "Bob" in this case.
The private variable we are passing can only be accessed within the {{#datainjection}}{{/datainjection}} block using the "#" decorator.
For example:
{{#datainjection}}{{#private}}{{/datainjection}}
will render the string "pirate"

Backbone Collection only fetched after executing alert

Hey I am new to backbone and Handlebars and something strange is happening with my code that I cannot figure out. Basically my node app queries mongo for some data which is then used by backbone and handlebars to relay the data to the user. When I try to relay the data to the user by using the fetch method, I get an error from handlebars stating that I cannot call on the compile method because I passed undefined. The strange thing is that when I was debugging the code it seemed like the collection was never being created i.e no data being returned from the backend. But when I tried to alert the collection the data does get returned and displayed which is really confusing? Anyway below are a few snippets of my code, would love to know why this is happening, thanks.
Init.js - used to initialize the backbone router
$(function(){
new appRouter();
Backbone.history.start({pushState: true});
});
photoClient.js Used to define and manage model, collection & views
var photo = Backbone.Model.extend({
idAtrribute: "id",
});
var photoCollection = Backbone.Collection.extend({
model: photo,
url: "/api/gallery"
});
var photoView = Backbone.View.extend({
tagName: "li",
className: "photo",
render: function(){
var template = $("#illustrationTemplate").html();
var compiled = Handlebars.compile(template);
var html = compiled(this.model.attributes);
this.$el.html(html);
return this;
}
});
var photoCollectionView = Backbone.View.extend({
initialize: function(){
this.listenTo(this.collection, "reset", this.render);
},
tagName: "ul",
className: "photos",
render: function(){
this.$el.html("");
this.collection.each(function(photo){
var photoV = new photoView({model: photo});
this.$el.append(photoV.render().el);
}, this);
return this;
}
});
var appRouter = Backbone.Router.extend({
routes: {
"gallery": "illustration"
},
illustration: function() {
var collection = new photoCollection();
collection.fetch({reset: true});
//IF I ADD THIS ALERT THE COLLECTION IS NOW DEFINED?
alert(collection);
console.log(collection);
var view = new photoCollectionView({collection: collection});
$(".app").html(view.render().el);
}
});
Illustration.jade
extends ../layout
block content
div.app
script(id = "illustrationTemplate", type = "text/x-handlebars")
{{image.created_at}}
I'd guess that the problem is right here:
$(".app").html(view.render().el);
As soon as you do that, everything that used to be inside <div class="app"> is gone. That includes your <script id="illustrationTemplate"> that you're using to store your template. Once your router's illustration method is called, $("#illustrationTemplate").html() will give you undefined and that view will no longer work.
You should store your templates inside <head> or at the very least outside of anything that your application will be writing to.

java.lang.NoClassDefFoundError at runtime when using express framework and node.js

I am trying to write a sample code to create an instance of a java class and then invoke a method using that instance. I am using node-java module to do this. The code compiles without any error. However when I hit the URL which actually hits the same code then I get the class not found exception.
I have verified that the jar and it is there in the same directory as index.js and the jar also contains the class file (Application.class) for which the instance is being created.
My index.js file
var java = require("java");
java.classpath.push("demo.jar");
var express = require('express');
var router = express.Router();
var Application = java.import('Application');
/* GET home page. */
router.get('/', function(req, res) {
var application = new Application();
var resp = application.getResponse();
res.render('index', { title: resp });
});
module.exports = router;
Sorry for my english. I had the same problem. Look [https://github.com/joeferner/node-java/issues/147]
my code:
`var java = require("java");
var path=require("path");
var fs=require("fs");
console.log("ruta in directory",path.join(__dirname));
console.log("exist file:",fs.existsSync(path.resolve(__dirname,"./lib-java/lib-tgd.jar")));
java.classpath.push("commons-lang3-3.1.jar");
java.classpath.push("commons-io.jar");
java.classpath.push(path.resolve(__dirname,"./lib-java/lib-tgd.jar"));
java.classpath.push(path.resolve(__dirname,"./lib-java/jackson-annotations- 2.5.1.jar"));
java.classpath.push(path.resolve(__dirname,"./lib-java/jackson-core-2.5.1.jar"));
java.classpath.push(path.resolve(__dirname,"./lib-java/jackson-databind-2.5.1.jar"));`
with path.resolve solves the problem of the file path

Get compiled jade template inside view?

I have a "partial" template that I want to use both client-side and server-side.
Is there some method or filter or something that's very similar to include except that instead of executing the template immediately, it returns a client-compiled function which I could then assign to a JS variable and use throughout my script?
At present, I'm doing this:
exports.list = function(req, res){
res.render('file/list', {
...
fileItemTemplate: jade.compile(fs.readFileSync(path.join(req.app.get('views'),'file','file-item.jade')), {client: true})
});
};
And then in my template I have:
ul#folder-list.thumbnails
each file in files
include file-item
...
script(type='text/javascript')
var fileItemTemplate = !{fileItemTemplate};
And in this way I can render some items to HTML on page-load, and then add some more in later by rendering the partial as data comes in.
This works, but it doesn't feel very DRY because I have to read in the file, and deal with filepaths and stuff in the route, and then essentially redeclare the exact same variable client-side.
Is there a nice way of doing this?
Something like this would be ideal:
script(type='text/javascript')
var fileItemTemplate = !{compile file-item};
A possible solution could be JadeAsset. See also the discussion here.
You can hook assets into Express:
assets.on('complete', function() {
var app = express.createServer();
app.configure(function() {
app.use(assets); // that's all you need to do
});
app.listen(8000);
});
To create your Jade assets:
var assets = new AssetRack([
new rack.JadeAsset({
url: '/templates.js',
dirname: __dirname + '/templates'
})
]);

How to use node modules (like MomentJS) in EJS views?

To use MomentJS in views/custom.ejs, what is the correct way (if any)?
Server side
routes/index etc we can easily use require('moment'); etc and it works fine.
Server Side (EJS views)
views/custome.ejs, something like <% var m = require('moment'); %> doesn't work
I am using ExpressJS with EJS as the template engine.
I found another way of doing this, and I think it has some advantages.
Don't polute your code exporting filters.
Access any method without the need to export them all.
Better ejs usage (no | pipes).
On your controller, or view.js do this:
var moment = require('moment');
exports.index = function(req, res) {
// send moment to your ejs
res.render('index', { moment: moment });
}
Now you can use moment inside your ejs:
<html>
<h1><%= moment().fromNow() %></h1>
</html>
I'm not an Node expert, so if anyone see something bad on doing this, let me know! :)
One more option:
This way you are setting the moment variable to a local available to all scripts in any EJS page on your site.
In your "index.js" (or "app.js") file do this: (after you have set up your 'app' with Express)
var moment = require('moment');
var shortDateFormat = "ddd # h:mmA"; // this is just an example of storing a date format once so you can change it in one place and have it propagate
app.locals.moment = moment; // this makes moment available as a variable in every EJS page
app.locals.shortDateFormat = shortDateFormat;
Then in your EJS file you can refer to moment (and shortDateFormat) as variables like this:
<%= moment(Date()).format(shortDateFormat) %>
Perhaps this is slightly more elegant?
var moment = require('moment');
app.locals.moment = moment;
Use in the view:
<%= moment(myDateValue).fromNow() %>
Now you can simply use moment in your EJS files.
I use moment on the server side with ejs. I wrote an ejs filter function that will return fromNow.
npm install moment
./views/page.ejs
<span class="created_at"><%=: item.created_at | fromNow %></span>
./routes/page.js
var ejs = require('ejs')
, moment = require('moment');
ejs.filters.fromNow = function(date){
return moment(date).fromNow()
}
You can create the function and attach it to the app.locals. and use it in the ejs template on the server side.
In your routes file you do
../routes/page.js
var ejs = require('ejs')
, moment = require('moment');
app.locals.fromNow = function(date){
return moment(date).fromNow();
}
../views/page.ejs
<span class="created_at"><%= fromNow(item.created_at) %></span>
Just remember to have moment added to to your package.json file
How about passing down require like this:
res.render('index', { require: require });
You might need to tweak to maintain the path:
res.render('index', { require: module => require(module /* here you may insert path correction */) });
Obviously this works with Node (backend) only.
The server side (EJS views) which you mentioned above is running on browser and not on your server. You cannot use require because browsers cannot understand it. You need to import the moment.js to use it
<script src="/js/moment.min.js"></script>
also i think it is good idea if you want you can add a middle-ware where you can add anything you want to the theme layer including user,config and moment:
// config, user, moment to the theme layer
app.use(function (req, res, next) {
// grab reference of render
var _render = res.render;
// override logic
res.render = function (view, options, fn) {
// extend config and continue with original render
options = options || {};
options.config = config;
options.moment = moment;
if (req.user && req.user.toJSON) {
options.user = req.user.toJSON();
}
_render.call(this, view, options, fn);
}
next();
});
I wrote a helpers to return moment for using on ejs view and layouts.
./helpers/utils/get-moment.js
const moment = require('moment');
module.exports = {
friendlyName: 'formatMoney',
description: 'format money number.',
inputs: {},
sync: true,
exits: {},
fn: function (inputs, exits) {
return exits.success(moment);
}
};
Then using:
const moment = sails.helpers.utils.getMoment();
As of Node v12.8.3, it seems that you can pass require directly to EJS templates, i.e. this works:
const ejs = require('ejs')
let renderedHTML = ejs.render(`<% const moment = require('moment') %>`, { require })

Resources