Best way to asynchronously load underscore templates - javascript

I'm planning to use backbone.js and underscore.js for creating website, and I will have lots of underscore templates:
<script type="text/template" id="search_template">
<p id="header">
//header content will go here
</p>
<p id="form">
<label>Search</label>
<input type="text" id="search_input" />
<input type="button" id="search_button" value="Search" />
</p>
<p id="dynamic_date">
//dynamic data will be displayed here
</p>
</script>
Of course my templates will be much more complicated.
Since I will have lots of them, I don't want to load all templates every time when page loads. I want to find a solution, where I can load specific template only when it will be used.
Another thing is that most of my templates will have same structure, only <p id="form"></p> and <p id="dynamic_date"></p> content will differ.
Could you please suggest me how should I do it?
Thanks,

Edit: I did some research and ported my iCanHaz code to underscore it also uses localStorage is available
Here is a github repository: https://github.com/Gazler/Underscore-Template-Loader
The code is:
(function() {
var templateLoader = {
templateVersion: "0.0.1",
templates: {},
loadRemoteTemplate: function(templateName, filename, callback) {
if (!this.templates[templateName]) {
var self = this;
jQuery.get(filename, function(data) {
self.addTemplate(templateName, data);
self.saveLocalTemplates();
callback(data);
});
}
else {
callback(this.templates[templateName]);
}
},
addTemplate: function(templateName, data) {
this.templates[templateName] = data;
},
localStorageAvailable: function() {
try {
return 'localStorage' in window && window['localStorage'] !== null;
} catch (e) {
return false;
}
},
saveLocalTemplates: function() {
if (this.localStorageAvailable) {
localStorage.setItem("templates", JSON.stringify(this.templates));
localStorage.setItem("templateVersion", this.templateVersion);
}
},
loadLocalTemplates: function() {
if (this.localStorageAvailable) {
var templateVersion = localStorage.getItem("templateVersion");
if (templateVersion && templateVersion == this.templateVersion) {
var templates = localStorage.getItem("templates");
if (templates) {
templates = JSON.parse(templates);
for (var x in templates) {
if (!this.templates[x]) {
this.addTemplate(x, templates[x]);
}
}
}
}
else {
localStorage.removeItem("templates");
localStorage.removeItem("templateVersion");
}
}
}
};
templateLoader.loadLocalTemplates();
window.templateLoader = templateLoader;
})();
Calling it would look something like:
templateLoader.loadRemoteTemplate("test_template", "templates/test_template.txt", function(data) {
var compiled = _.template(data);
$('#content').html(compiled({name : 'world'}));
});
Here is my original answer
Here is a method I wrote for ICanHaz (mustache) that performs this exact function for the same reason.
window.ich.loadRemoteTemplate = function(name, callback) {
if (!ich.templates[name+"_template"]) {
jQuery.get("templates/"+name+".mustache", function(data) {
window.ich.addTemplate(name+"_template", data);
callback();
});
}
else {
callback();
}
}
I then call it like so:
ich.loadRemoteTemplate(page+'_page', function() {
$('#'+page+'_page').html(ich[page+'_page_template']({}, true));
});

I really like the way the stackoverflow team has done templating with the mvc-miniprofiler. Take a look at these links:
Includes.js (Github link)
Includes.tmpl (Github link)
They use the local storage to cache the templates locally if your browser supports local storage. If not they just load it every time. Its a pretty slick way to handle the templates. This also allows you to keep your templates that aren't required immediately in a separate file and not clutter up your html.
Good luck.

Although both of the above answers work, I found the following to be a much simpler approach.
Places your templates wrapped in script tags into a file (say "templates.html") as follows:
<script type="text/template" id="template-1">
<%- text %>
</script>
<script type="text/template" id="template-2">
oh hai!
</script>
Then the following bit of javascript:
$(document).ready(function() {
url ='http://some.domain/templates.html'
templatesLoadedPromise = $.get(url).then(function(data) {
$('body').append(data)
console.log("Async loading of templates complete");
}).fail(function() {
console.log("ERROR: Could not load base templates");
});
});
Which then let's you select your templates quite simply using the IDs you previously defined. I added the promise
$.when(templatesLoadedPromise).then(function() {
_.template($('#template-1').html(), {'text':'hello world'} )
});
You can then extend this and load multiple files if you want.
As a side note, I've found that any core templates needed for initial page render are better embedded in the HTML (I use tornado modules on the server) but that the above approach works very nicely for any templates needed later (e.g., in my case the templates for a registration widget which I want to use across pages is perfect for this as it'll only be loaded on user interaction and is non-core to the page)

Related

How to get data into the HTML page of an ExtensionSidebarPane?

I have successfully created a ExtensionSidebarPane which displays some data I have in stored in local storage:
saving the data:
localStorage.setItem('savedDataForReview',JSON.stringify(savedDataObject));
creating and updating the panel:
chrome.devtools.panels.elements.createSidebarPane("Data Reviewing Panel",
function() {
chrome.devtools.inspectedWindow.eval(
'localStorage.getItem("savedDataForReview")',
function (result) {
pane.setObject(JSON.parse(result));
}
);
});
This works nicely. However I now want to improve the pane to have some tables and buttons, so I need to use HTML instead of just setting a JSON object. So I need to use
pane.setPage('html/my_reviewing_pane.html');
in place of the setObject line.
This works, however I can't seem to get access to local storage from within that pane HTML page.
If I include some javascript to access my saved data localStorage.getItem('savedDataForReview') then it returns null.
How can I pass data into the HTML page on an ExtensionSidebarPane? I imagine this is a scoping issue but I am not sure how I can get around it. I can't even easily build the HTML from within the function where the data is in scope, because setPage takes a local file path rather than an HTML string.
Hope someone can help.
In the end, I went with using a background script and messaging passing. Thank you #wOxxOm for the pointers.
I added a background script background.js, adding to the manifest.json:
...
"background": {
"scripts":["background.js"],
"persistent":false
},
...
In my background.js script I added the following code so that it can receive messages from anywhere in the extension (content scripts, panel JS or pane JS):
chrome.runtime.onMessage.addListener(function(rq, sender, sendResponse) {
if (rq.action=="getData") {
sendResponse(getData());
return true;
} else if (rq.action=="deleteData") {
sendResponse(deleteData());
} else if (rq.action=="storeData") {
sendResponse(storeData(rq.data));
} else {
console.log('received unrecognized request:');
console.dir(rq);
sendResponse(null);
}
return true;
});
function storeData(object) {
localStorage.setItem("savedDataForReview",JSON.stringify(object));
return true;
}
function getData() {
return JSON.parse(localStorage.getItem('savedDataForReview'));
}
function deleteData() {
localStorage.removeItem('savedDataForReview');
return true;
}
Then within my content script I send storage messages to the background script like this:
function getData(callback) {
chrome.extension.sendMessage({action: "getData"}, function(resp) {
callback(resp);
});
}
function storeData(callback, data) {
chrome.extension.sendMessage({action: "storeData", data: data}, function(resp) {
callback(resp);
})
}
function deleteData(callback) {
chrome.extension.sendMessage({action: "deleteData", data: data}, function(resp) {
callback(resp);
})
}
storeData(function(response) {
console.log(`response from storing data was: ${response}`);
},data);
And in my pane's HTML page my_reviewing_pane.html I include a javascript file:
<h2>Reviewing Pane</h2>
<div id="review_table">
</div>
<script src="my_reviewing_pane.js"></script>
In that javascript file my_reviewing_pane.js I access the stored data by sending a message to the background script like this:
function getData(callback) {
chrome.extension.sendMessage({action: "getData"}, function(resp) {
callback(resp);
});
}
getData(function(savedDataFromStorage) {
let tableDiv = document.getElementById("review_table");
console.dir(savedDataFromStorage);
// now set HTML content into tableDiv.innerHTML using data from savedDataFromStorage...
});
Finally I create and set the pane from within the javascript of my devtools page:
chrome.devtools.panels.elements.createSidebarPane("Reviewing Pane",
function (pane) {
pane.setPage('my_reviewing_pane.html');
}
);
I decided to use HTML5 Local Storage rather than Chrome storage because I didn't need syncing and didn't want the size limits - but I think Chrome storage looks more powerful for a lot of use cases. See this article.
Hope this is useful to someone else.

How to add p5.dom to p5.js in instance mode

I'm using requirejs to bundle my files. I've used p5.js for some time, but I need to add p5.dom now. The problem is I don't know how to do it with my requirejs. I couldn't find any information how to do it with bundling.
I have something like this:
require(['lib/p5js/lib/p5'], function (p5) {
var myp5 = new p5(function (_sketch) {
_sketch.setup = function() {
????.createButton('click me'); // I need to access p5.dom here.
}
});
});
EDIT: When I try to use it like a variable I get undefined.
require(['lib/p5js/lib/p5', 'lib/p5js/lib/addons/lib/p5.dom'], function (p5, p5Dom) {
var myp5 = new p5(function (_sketch) {
_sketch.setup = function() {
p5Dom.createButton('click me'); // p5Dom is undefined.
}
});
});
I used shim to ensure that p5.js is loaded before p5.dom is loaded, but it didn't help:
shim: {
'lib/p5js/lib/addons/p5.dom': {
deps: ['lib/p5js/lib/p5']
}
},
I found a solution. Unfortunately I wasn't able to do it with requirejs, but I simply included script tags before requirejs and it solved the problem. So it looks like this:
<script src="~/lib/p5js/lib/p5.js"></script>
<script src="~/lib/p5js/lib/addons/p5.dom.js"></script>
<script src="~/lib/requirejs/require.js" data-main="/js/scripts/tetromino-client/client.js"></script>
I hoped to do this in requirejs, but I don't understand why it didn't work.

Writing callbacks for a wizard?

I'm building a custom wizard in knockout that dynamically loads knockout components during each "step" of the wizard. I've managed to get all of this working without much hassle, and it seems to work pretty well.
However, I'm wanting to implement some callbacks within the wizard when certain events happen. For example, before and after navigation.
Currently, one of my navigation functions looks like this:
this.goNext = function () {
if (!this.canGoNext()) return;
this.currentStep(this.pages()[this.currentIndex() + 1]);
this.loadPage();
}
I would like to build 2 callback functions called beforePageChange and onPageChange.
My general assumption is that beforePageChange would pass in a couple parameters, notably the current page and the next page. However, I also want it to be able to be observed from any other class utilization the wizard.
For example, on my parent page I would have something like:
this.wizard = Site.loadWizard(arguments);
this.wizard.beforePageChange(function(options) {
if (!options.currentPage.complete) return false;
// do stuff
return true;
});
In turn the wizard would execute its navigation commands and trigger the appropriate callbacks.
I feel like there's something I'm just fundamentally missing here.
Edit
My current version works as follows:
In the wizard:
this.canChangePage = ko.obserable(true);
this.beforePageChange = function (options) {
};
this.beforePageChangeHandler = function (options) {
this.beforePageChange(options);
// do stuff
return true;
};
this.onPageChange = function (options) {
};
this.onPageChangeHandler = function (options) {
this.onPageChange(options);
//do stuff
return true;
}
On the page implementing the wizard:
this.wizard = Site.loadComponent(params, function () {
this.wizard.beforePageChange = function (options) {
this.canChangePage(false);
};
}.bind(this));
I'm not sure if there's a better way to implement this, or if this is the best solution.
The solution Tomalak described in their comment (I think):
Since you already have access to the wizard instance, you can subscribe to its currentStep observable. To get notifications before it changes, you pass a third parameter: "beforeChange". (The second is the this context).
var Wizard = function() {
this.currentStep = ko.observable(0);
}
Wizard.prototype.next = function() {
this.currentStep(this.currentStep() + 1);
}
Wizard.prototype.prev = function() {
this.currentStep(this.currentStep() - 1);
}
var Page = function() {
this.wizard = new Wizard();
this.wizard.currentStep.subscribe(function(oldStep) {
console.log("Page work before step change from step", oldStep);
}, this, "beforeChange");
this.wizard.currentStep.subscribe(function(newStep) {
console.log("Page work after step change to", newStep);
});
}
ko.applyBindings(new Page());
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
<div data-bind="with:wizard">
<button data-bind="click: prev">back</button>
<span data-bind="text: currentStep"></span>
<button data-bind="click: next">next</button>
</div>

Using jquery before the script includes?

In my one of the PHP project, part of it done by some other people. All the js scripts are loaded in footer file. The html closes at the footer file.
How Can I use jquery functions before the inclusion of footer file ?. Since using it after the footer file will show it after the closing of html. When I tired to use jquery functions before the footer I get the js error $ not defined etc.. I have searched many similar questions but couldn't find a solution. I can't change the file inclusion order currently since some other people have done it. Any help will be appreciated.
If, like you mention, you have absolutely no control over where jQuery scripts are included, you can set timer to check when jQuery is ready, e.g.:
<script>
var i = setInterval(function() {
if ($) {
clearInterval(i);
$(document).ready(function(){
alert('Ready!');
})
}
}, 100)
</script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
But otherwise - if you can control it - include them before your code.
this code will only run properly in modern browsers (IE > 8)
<div class="jq"></div>
<script>
document.addEventListener('DOMContentLoaded', function(){
$('.jq').html('working');
});
</script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
Example - http://jsfiddle.net/2mmdrega/
I sometimes did something like this:
function JQueryManager(callback, thisArgument) {
this.execute = function() {
if (typeof jQuery == "undefined") {
console.log("Waiting for jQuery...");
var self = this;
var selfArgs = arguments;
setTimeout(function() {
self.execute(selfArgs)
}, 100)
} else {
console.log("Found jQuery. Version: " + jQuery.fn.jquery);
callback.apply(thisArgument, arguments)
}
}
}
function myCallback(results) {
alert("found " + results.length + " results")
}
var jQueryManager = new JQueryManager(myCallback);
jQueryManager.execute($("div"));
That allows you to execute a callabck function when jQuery is available
EDIT: fixed some copy-paste errores

jquery.get function url parameter

in the jquery.get() function, the first parameter is URL, is that the url to the content I want to retrieve or to the Controller/action method.
The problem is I'm building an asp.net-mvc application and I'm not sure what to pass as this parameter. Right now I'm passing my partialview.cshtml but nothing is being returned, not sure if I'm doing this right or wrong.
Here's what I got
<div id="editor_box"></div>
<button id="add_button">add</button>
<script>
var inputHtml = null;
var appendInput = function () {
$("#add_button").click(function() {
if (!inputHtml) {
$.get('AddItem.cshtml', function (data) {
inputHtml = data;
$('#editor_box').append(inputHtml);
});
} else {
$('#editor_box').append(inputHtml);
}
})
};
</script>
also, what is the second parameter "function (data)" is this the action method?
You need to remove var appendInput = function () { from the script. You are defining a function but never calling it. Just use the following (update you action and controller) names
<script>
var inputHtml = null;
$("#add_button").click(function() {
if (!inputHtml) {
$.get('#Url.Action("SomeAction", "SomeController")'', function (data) {
inputHtml = data;
$('#editor_box').append(inputHtml);
});
} else {
$('#editor_box').append(inputHtml);
}
});
</script>
Edit
Based on your script you appear to be requiring the content only once (you then cache it and add it again on subsequent clicks. Another alternative would be to render the contents initially inside a hidden <div> element, then in the script clone the contents of the <div> and append it to the DOM
<div id="add style="display:none">#Html.Partial("AddItem")</div>
$("#add_button").click(function() {
$('#editor_box').append($('add').clone());
});
The first argument to $.get is the URL which will respond with the expected data. jQuery/JavaScript don't care what kind of server side architecture you have or the scripting language. Whether the URL looks like a file AddItem.cshtml or a friendly route like /User/Sandeep, it doesn't matter as far as the client side is concerned.
In the case of ASP.NET, your URL endpoint can be generated like so:
$.get('#Url.Action("SomeAction", "SomeController")', function (data) {

Categories

Resources