I am trying to write an adapter for a client-side HTML/JS templating system to use dust.js under the hood. Unfortunately the API expects render operations to occur synchronously: the rendered output should be returned from the render() call. Dust.js is asynchronous and passes render output to a callback function. Is there any way to work around this, either in the Dust APIs or through some crazy Javascript hack?
DustJS is only going to execute things asynchronously when the resources it needs to render (templates, partials) haven't already all been loaded.
If all the dependencies of a template are loaded before you execute that template then it'll execute synchronously (as far as I can tell anyhow). So you can do something like:
var result;
dust.render("tpl", data, function(err, res) {
result = res;
});
console.log(result); // result will actually already be filled out if dustjs didn't
// have to go look for resources somewhere.
Here is a fuller example below:
(and here is a jsfiddle link so you can run it: http://jsfiddle.net/uzTrv/1/)
<script type="text/javascript" src="dust.js"></script>
<script>
var tpl = dust.compile("Omg {#people} {.} {/people} are here! {>partial/}", "tpl");
var partial = dust.compile("I'm a partial but I've already been included so things still run {how}", "partial");
dust.loadSource(tpl);
dust.loadSource(partial);
var data = {
people: ["jim", "jane", "jack", "julie"],
how: "synchronously!"
};
var result;
dust.render("tpl", data, function(err, res) {
result = res;
});
console.log(result);
</script>
There could be cases (besides the one I mentioned) where I'm wrong... I don't know everything about dustjs.
I too wanted to have a function that accepted a context and returned the dust rendered text. Here is the solution I came up with:
// This function sets up dust template, and returns a new function "dusterFn()"
// dusterFn() can be passed a Context, and will return the rendered text.
// #param {String} text: The template text.
// #param {String} [name]: The name of the template to register with dust. If none is provided, a random number is used.
// #param {Function} [onError]: A function that is called if an error occurs during rendering.
function getDusterFn(text, name, onError) {
var dusterFn = null;
name = name || Math.floor(Math.random() * 99999).toString();
onError = onError || function (error) { };
try {
var compiled = dust.compile(text, name)
dust.loadSource(compiled);
dusterFn = function (context) {
var dustOutput = '';
dust.render(name, context, function (error, out) {
if (error) onError(error);
dustOutput = out;
});
return dustOutput;
};
} catch (e) {
// invalid template syntax
e += "\n\nPlease check your template syntax.";
throw (e);
}
return dusterFn;
}
Usage
var greetingTemplate = getDusterFn('Hello {name}, You are {age} years old!');
greetingTemplate({name: 'Jane', age: 24});
Matt's solution gave me some pointers on how to write a little wrapper that hides the "ugliness" of his solution (by "ugliness" I mean declaring variable outside of callback, assigning value inside callback and returning outside callback).
It not only wraps the hack into a little function but also binds the templateĀ“s name. I find this incredible helpful as I find myself using the same render function over and over again, but I do not want to specifiy the templateĀ“s name every time.
function templates(template) {
return function templatesWrapper(data) {
var result;
dust.render(template, data, function onRender(err, data) {
if (err) {
throw err;
}
result = data;
});
return result;
}
}
This is how to use it:
var renderHello = templates('hello.html');
renderHello({ username: 'Stackoverflow' });
// => <h1>Hello, Stackoverflow</h1>
Related
I made an input that let me filter a table of softwares.
<input type="text" id="softwares-search" class="form-control" aria-label="Input de recherche" aria-describedby="softwares-search">
Then in javascript my filter work well if I console.log(....)
But when I replace it with a return, nothing is returned. I think it is due to my var affectation through the event listener :
const maxwell = () => {
search = document.querySelector('#softwares-search').value;
return softwares.filter(row => row.name.includes(search) || row.description.includes(search));
}
const softwaresSearch = document.querySelector('#softwares-search');
if (softwaresSearch) {
var results = softwaresSearch.addEventListener('keyup', maxwell)
console.log(results);
}
Thank all
EDIT 1 :
I was so angry, so blind, I had S#!t in my eyes, no need to use a global :(
const softwaresSearch = document.getElementById('softwares-search');
if (softwaresSearch) {
softwaresSearch.addEventListener('keyup', (e) => {
search = document.getElementById('softwares-search').value;
var filtredSoftwares = softwares.filter(e => e.name.includes(search) || e.description.includes(search) );
renderTable(filtredSoftwares);
});
}
const renderTable = (softwares) => {
Object.values(softwares).forEach(value=>{
console.log(value);
});
// Todo build HTML table
}
Instead of returning I think you just need to replace the current array like this
const maxwell = () => {
search = document.querySelector('#softwares-search').value;
softwares = softwares.filter(row => row.name.includes(search) || row.description.includes(search));
}
And results is not needed:
const softwaresSearch = document.querySelector('#softwares-search');
if (softwaresSearch) {
softwaresSearch.addEventListener('keyup', maxwell)
}
As far as I know, softwareSearch.addEventListener won't return anything, since that is an event listener, and does not return any value. It simply executes the function passed in the 2nd parameter. You could try doing this instead
softwaresSearch.addEventListener('keyup', () => {
var results = maxwell();
console.log(results);
});
What this would do is that, it would call your maxwell function when the keyup event, since that is what it looks you are trying to do.
Please share all relevant code before posting a question, this code includes the variable "softwares" that exist outside what is visible to us.
Additionally, there are some issues with your code.
I don't understand your naming of the maxwell function. You should name functions as verbs, not anything else. A function is a machine that is doing something, and possibly returning something. It should be named to what it is doing.
On the second line, you say "search = ... ", but you didn't declare it as a variable.
You are returning something based on a value that isn't validated ('search' can be either undefined or a string value in this case), hence, your return will most likely just return undefined and not any value at all.
Your function can possibly not return anything at all since you are returning something within your if-statement. You can use a closure to always return something.
I would also suggest passing a search string as a variable to your function that should return a list based on the search query. Getting in the habit of short, concise functions with expected inputs/outputs, will make your code more readable and less error-prone and less likely to produce unwanted side-effects.
I don't know the rest of your code, but I don't recommend assigning variables in the global scope. Your "maxwell", "softwareSearch" variables both exist in the global space, unless you have wrapped them in another function block already (such as jquerys $(document).ready(() => { ...everything here is scoped })
You are getting the same element in two different places in your code.
Here is an updated code sample, but I can't test it since I don't know the rest of your code.
/*
* Executing the whole thing in this IIFE will make all variables declared inside here scoped to this block only,
* thus they can't interfere with other code you may write
*/
(() => {
const listOfSoftwares = softwares; // --- get your softwares variable here somehow, I don't know where "software" comes from.
// input element
const search = document.querySelector('#softwares-search');
/**
* Filter search results
* #param {string} query Search query
* #returns {Array} The results array
*/
const filterSoftwareSearchResults = (query) => {
let results = [];
results = listOfSoftwares.filter(software => software.description.includes(query) || software.title.includes(query))
// Verify
console.log(results);
// Should return array of results, even if empty
return results;
}
if (search) {
search.addEventListener('keyup', () => {
filterSoftwareSearchResults(search.value)
})
}
})()
The addEventListener function always returns undefined, so your results variable is undefined.
Returning from the callback function (maxwell) is also of no use.
You either need to do something with the data inside of your callback, or maybe pass the data to a global variable.
getting error "could not locate bindings file"
trying to search for blog posting, then lookup banner based on blog abbrev, then write out
without banner lookup, everything works fine
think we are screwing up the callback but can't find good example
app.get("/posting/:id", function(req,res) {
var db = app.get('db');
var id = [req.params.id];
console.log(id);
db.run("select * from postings where id=$1",[1], function(err,posting) {
console.log(posting);
var choice = posting[0].choice;
var banner = banner_lookup(choice, function(err, banner) {
console.log(banner);
res.render("posting", {posting:posting[0], banner:banner});
});
});
});
function banner_lookup(abbrev) {
console.log("banner lookup");
for (i=0;i++;i<banners.length) {
if (abbrev == banners[i].abbrev) {
console.log(banners[i]);
return (banners[i]);
}
}
return {"name":"","color":"#000"};
}
var banners = [{"abbrev":"aaa","name":"Newsletter A", "color":"#888888"}];
The problem lies in this part of the program. You are basically passing two arguments to the banner_lookup function - the variable choice and the callback function where you expect the banner to be available after the lookup
var banner = banner_lookup(choice, function(err, banner) {
console.log(banner);
res.render("posting", {posting:posting[0], banner:banner});
});
However the actual function definition of banner_lookup accepts only one named argument - the choice. The banner_lookup function calculates and returns the banner value but it is not being passed to the callback function like you expect it to. You will have to explicitly write the functionality to support callback functionality in banner_lookup
Your code (with callback functionality) would look like this
function banner_lookup(abbrev, callback) {
console.log("banner lookup");
var bannerOutput = {"name":"","color":"#000"};
for (i=0;i++;i<banners.length) {
if (abbrev == banners[i].abbrev) {
console.log(banners[i]);
bannerOutput = banners[i];
}
}
if(typeof callback == "function") {
callback(null, bannerOutput);
} else {
return bannerOutput;
}
}
The above method now accepts a callback parameter, and if it is defined as a function, then passes the output to the function else returns it like earlier.
Tip 1 : You don't really need to implement callback functionality if the banner_lookup function is doing simple tasks in sync. You can simply write
var banner = banner_lookup(choice);
console.log(banner);
This banner will have the expected result.
Tip 2 : And as Amit pointed out in his comment, read about SQL injection attacks. Make sure you sanitize all variables that go into making a DB query (You are directly accepting the id parameter from the route and passing it to the DB query which can be easily misused.
Your function banner_lookup requires only 1 parameter on its decleration, yet you pass it 2 when you call it (choise and the callback function).
I'm building a plugin that fetches info for a bunch of images in JSON, then displays them in some dialog for selection. Unfortunately, my first intuition pretty clearly results in a race condition:
var ImageDialog = function () {};
ImageDialog.prototype.items = [];
ImageDialog.prototype.fetch_images() {
var parse_images = function(data) {
// Magically parse these suckers.
data = awesome_function(data);
this.items = data;
};
magicalxhrclass.xhr.send({"url": 'someurl', "success": parse_images, "success_scope": this});
}
ImageDialog.prototype.render = function () {
this.fetch_images();
// XHR may or may not have finished yet...
this.display_images();
this.do_other_stuff();
};
var monkey = new ImageDialog();
monkey.render();
Off of the top of my head, I think I could fix this by changing the parse_images callback to include the rest of the render steps. However, that doesn't look quite right. Why would the fetch_images method be calling a bunch of things about displaying images?
So: what should I do here?
I am pretty certain deferreds would help, but alas: I need to write this without any external libraries. :(
Comments on other code smells would be nice, too!
In general, the basic idea you can use is that when a regular program would use a return statement (meaning, "My function is done now do your job!") an asynchronous continuation-passing program would instead use a ballcabk function that gets explicitly called
function fetch_images(callback){
magicalXHR({
success: function(data){
parse_images(data);
callback(whatever);
}
}
}
or, if parse_images is itself an async function:
parse_images(data, callback)
Now when you call fetch_images the code after it goes into a callback instead of assuming that fetch_images will be done when it returns
fetch_images(function(
display_images()
})
By using callbacks you can emulate pretty well what a traditional program could do (in fact its a fairly mechanical translation between one form of the other). The only problem you will now encounter is that error handling gets tricky, language features like loops don't play well with async callbacks and calbacks tend to nest into callback hell. If the callbacks start getting too complex, I would investigate using one of those Javascript dialects that compiles down to continuation-passing-style Javascrit (some of them work without needing extra libraries at runtime).
How about this?
var ImageDialog = function () {
this.items = []; // just in case you need it before the images are fetched
};
ImageDialog.prototype.fetch_images(callback) {
var that = this;
function parse_images (data) {
// Magically parse these suckers.
data = awesome_function(data);
that.items = data;
callback.apply(that);
};
magicalxhrclass.xhr.send({"url": 'someurl', "success": parse_images, "success_scope": this});
}
ImageDialog.prototype.render = function () {
this.fetch_images(function(){
this.display_images();
this.do_other_stuff();
});
};
var monkey = new ImageDialog();
monkey.render();
Here's a thought about what to do.
ImageDialog.prototype.fetch_images() {
var parse_images = function(data) {
// Magically parse these suckers.
data = awesome_function(data);
this.items = data;
fetch_images.caller() // Unfortunately, this is nonstandard/not in the spec. :(
};
magicalxhrclass.xhr.send({"url": 'someurl', "success": parse_images, "success_scope": this});
}
ImageDialog.prototype.render = function () {
if (this.items === []) {
this.fetch_images()
return;
} else {
this.display_images();
this.do_other_stuff();
};
};
This way I'm not passing some implementation detail to fetch_images, and I get caching, to boot. Am I still trying too hard to escape CPS, or is this sensible?
I'm discovering the concept of "objects" in JavaScript. I'm making an RSS Parser, and I have an error (commented).
function MyParser (feed_url) { // Construct
"use strict";
this.feedUrl = feed_url;
this.pubArray = [];
if (typeof (this.init_ok) == 'undefined') {
MyParser.prototype.parse = function () {
"use strict";
var thisObj = this;
$.get(this.feedUrl, function (data, textStatus, jqXHR) {
if (textStatus == 'success') {
var xml = jqXHR.responseXML,
//lastBuildDate = new Date($(xml).find('lastBuildDate').text());
items = $(xml).find('item');
items.each(function () {
var pubSingle = thisObj.makeObj($(this).find('pubDate').text(),
$(this).find('link').text(),
$(this).find('title').text(),
$(this).find('description').text(),
$(this).find('encoded').text(),
$(this).find('commentRss').text(),
$(this).find('comments').last().text());
thisObj.pubArray.push(pubSingle);
});
console.log(thisObj.pubArray); // OK
}
}, 'xml');
console.log(this.pubArray); // Empty
return (this.pubArray);
};
MyParser.prototype.makeObj = function (pubDate, pubLink, pubTitle, pubDesc, pubContent, pubComCount, pubComLink) {
"use strict";
var pubSingle = {};
pubSingle.pubDate = new Date(pubDate);
pubSingle.pubLink = pubLink;
pubSingle.pubTitle = pubTitle;
pubSingle.pubDesc = pubDesc;
pubSingle.pubContent = pubContent;
pubSingle.pubComCount = pubComCount;
pubSingle.pubComLink = pubComLink;
return (pubSingle);
};
}
this.init_ok = true;
}
If you look at the console.log(), you'll see that the line // OK is outputting my array correctly.
But later, when returning from $.get, my array is empty.
Does anybody have an idea why, and how to correct that please?
This is not a problem with variable-scope. The problem here is that you're working with asynchronous flow and you're not thinking correctly the flow.
Let me explain:
When you do your .get, you fire a parallel asynchronous process that will request information from the browser, but your main program's flow keeps going, so when you get to your "return" statement, your array has not been filled yet with the response from your get method.
You should use your array from inside the get callback and not outside of it, since you can't guarantee that the array will have the information you need.
Does it make any sense?
Let me know!
Further explanation
According to your comments, you're still doing something like this:
var results = MyParser(feed_url);
//code that uses results.pubArray
And you cannot do that. Even though you're setting your "pubArray" inside your .get callback, you're trying to use pubArray right after you called MyParser and that's before the .get callback is called.
What you have to do, is call your next step on your program's logic from within the .get callback... that's the only way you can be sure that the pubArray is filled with proper data.
I hope that makes it clearer.
This is because your line
console.log(this.pubArray); // Empty
is being called directly after you issue your Ajax request; it hasn't had time to fetch the data yet. The line
console.log(thisObj.pubArray); // OK
is being called inside the Ajax callback, by which time the data has been fetched.
Thank you all, and particulary #Deleteman .
Here is what I did:
$.get(this.feedUrl, 'xml').success(function () {
thisObj.handleAjax(arguments[0], arguments[1], arguments[2]);
$(document).trigger('MyParserDone');
}).error(function () {
$(document).trigger('MyParserFailed');
});
Then, when i enter "HandleAjax", i'm back in my object context, so "this" refers to my object and the right properties. The only "problem" is that I have to set a listener (MyParserDone) to make sure the parsing is finished.
I am currently in the process of making my first Titanium iPhone app.
In a model I got:
(function() {
main.model = {};
main.model.getAlbums = function(_args) {
var loader = Titanium.Network.createHTTPClient();
loader.open("GET", "http://someurl.json");
// Runs the function when the data is ready for us to process
loader.onload = function() {
// Evaluate the JSON
var albums = eval('('+this.responseText+')');
//alert(albums.length);
return albums;
};
// Send the HTTP request
loader.send();
};
})();
and I call this function in a view like:
(function() {
main.ui.createAlbumsWindow = function(_args) {
var albumsWindow = Titanium.UI.createWindow({
title:'Albums',
backgroundColor:'#000'
});
var albums = main.model.getAlbums();
alert(albums);
return albumsWindow;
};
})();
however it seems like the call to the model (which fetches some data using HTTP) doesn't wait for a response. In the view when I do the alert it haven't received the data from the model yet. How do I do this in a best-practice way?
Thanks in advance
OK,
Something like this,
function foo(arg1, callback){
arg1 += 10;
....
... Your web service code
....
callback(arg1); // you can have your response instead of arg1
}
you will call this function like this,
foo (arg1, function(returnedParameter){
alert(returnedParameter); // here you will get your response which was returned in above function using this line .... callback(arg1);
});
so here arg1 is parameter (simple parameter like integer, string etc ... ) and second argument is your call back function.
Cheers.
What you need is Synchronous call to web service, so that it will wait till you get the response from the service.
To achieve this in java script you have to pass callback function as parameter and get the return value in callback function instead of returning value by return statement.
Actually coding style you are using is new for me because i am using different coding style.
But the main thing is you have to use call back function to retrieve value instead of return statement. Try this and if you still face the problem than tell me i will try to give an example.
the callback way like zero explained is nicely explained, but you could also try to get it handled with events.
(function() {
main.ui.createAlbumsWindow = function(_args) {
var albumsWindow = Titanium.UI.createWindow({
title:'Albums',
backgroundColor:'#000'
});
var status = new object(), // eventlistener
got_a_valid_result = false;
// catch result
status.addEventListener('gotResult',function(e){
alert(e.result);
got_a_valid_result = true;
});
// catch error
status.addEventListener('error',function(e){
alert("error occured: "+e.errorcode);
git_a_valid_result = true;
});
var albums = main.model.getAlbums(status);
// wait for result
while (!got_a_valid_result){};
return albumsWindow;
};
})();
and your model may something like
main.model.getAlbums = function(status) {
var loader = Titanium.Network.createHTTPClient();
loader.open("GET", "http://someurl.json");
loader.onload = function() {
var albums = eval('('+this.responseText+')');
status.fireEvent('gotResult',{result:albums});
return albums;
};
loader.onerror = function(e){
status.fireEvent('error',{errorcode:"an error occured"});
};
// Send the HTTP request
loader.send();
};
Just as a suggestion, try to use JSON.parse instead of eval as there are risks involved with using eval since it runs all javascript code.
I think that the solution The Zero posted is likely better for memory management, but I'm not totally sure. If you do and eventListener, be aware of the following
(see https://wiki.appcelerator.org/display/guides/Managing+Memory+and+Finding+Leaks)
function doSomething(_event) {
var foo = bar;
}
// adding this event listener causes a memory leak
// as references remain valid as long as the app is running
Ti.App.addEventListener('bad:idea', doSomething);
// you can plug this leak by removing the event listener, for example when the window is closed
thisWindow.addEventListener('close', function() {
// to remove an event listener, you must use the exact same function signature
// as when the listener was added
Ti.App.removeEventListener('bad:idea', doSomething);
});