How to pass JQuery instance in a Handlebars template context? - javascript

I would like to use Handlebars to insert nested elements which could be jQuery elements such as:
var ul = Handlebars.compile($('#ul').html());
var li = Handlebars.compile($('#li').html());
let el = $(li({slot: 'Hello World!'})).data('foo', Object([42]))
$('#main').append(ul({slot: el}))
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/handlebars.js/4.1.2/handlebars.min.js"></script>
<div id="main" class="container">
</div>
<script id="ul" type="text/x-handlebars-template">
<ul>
{{{slot}}}
</ul>
</script>
<script id="li" type="text/x-handlebars-template">
<li>
{{slot}}
</li>
</script>
Unfortunately I am struggling with two issues:
Handlebars expect a html content in the context, therefore I loose data-foo.
Using {{{slot}}} might be unsafe
What would be the alternative?

To achieve expected result, use below option using
Use attr instead of data(), as it only stores value and it will not set attribute on DOM
refer this link for more details - jQuery data attr not setting
Split appending ul and el
Code sample - https://codepen.io/nagasai/pen/PvrLba?editors=1010
var ul = Handlebars.compile($('#ul').html());
var li = Handlebars.compile($('#li').html());
let el = $(li({slot: 'Hello World!'})).attr('data-foo', Object([42]))
$('#main').append(ul)
$('#main ul').append(el)
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/handlebars.js/4.1.2/handlebars.min.js"></script>
<div id="main" class="container">
</div>
<script id="ul" type="text/x-handlebars-template">
<ul>
{{{slot}}}
</ul>
</script>
<script id="li" type="text/x-handlebars-template">
<li>
{{slot}}
</li>
</script>

One solution to this problem is write a wrapper that look for jQuery instances in the template context. It replaces every instances with a plain text unique identifier then insert back the jQuery instances to the rendered template:
Handlebars.jQueryCompile = (template) => {
let jQueryObjects = []
let tag = id => `jQueryObject(${id})`
let alter = (obj) => {
if (!(obj instanceof Object)) return
for (var prop in obj) {
if (obj[prop] instanceof jQuery) {
jQueryObjects.push(obj[prop])
obj[prop] = tag(jQueryObjects.length - 1)
} else {
alter(obj[prop])
}
}
}
return (context) => {
alter(context)
let obj = $(template(context))
jQueryObjects.forEach((jq, id) => {
obj.find('*:contains("' + tag(id) + '")').html('').append(jq)
})
return obj
}
}
Here your updated example:
var ul = Handlebars.jQueryCompile($('#ul').html());
var li = Handlebars.jQueryCompile($('#li').html());
let el = $(li({slot: 'Hello World!'})).data('foo', Object([42]))
$('#main').append(ul({slot: el}))
Notice that you don't need to use the {{{}}} mustaches because plain text is inserted instead of html elements.

Related

Jquery converted to Javascript results in title undefined? How do I convert this properly?

I'm trying to convert my jquery back to Javascript, but for some reason it states that title is undefined. I'm not sure how to convert it properly or what to do to fix this issue.
Here is the current jquery code
update: function (e) {
var el = e.target;
var $el = $(el);
var val = $el.val().trim();
if (!val) {
this.destroy(e);
return;
}
if ($el.data('abort')) {
$el.data('abort', false);
} else {
this.todos[this.indexFromEl(el)].title = val;
}
this.render();
},
Here is the code from indexFromEl function
indexFromEl: function (el) {
var id = $(el).closest('li').data('id');
var todos = this.todos;
var i = todos.length;
while (i--) {
if (todos[i].id === id) {
return i;
}
}
},
So based off the code above, I tried to convert it myself, but I don't think I did it correctly.
update: function (e) {
var el = e.target;
var val = el.value.trim();
if (!val) {
this.destroy(e);
return;
}
if(val === 'abort') {
return false;
} else {
return this.todos[this.indexFromEl(el)].title = val;
}
this.render();
},
How do I convert the first code block from jquery to javascript? Also, I'm not sure how to edit the first line in the indexFromEl jquery code
Here is the jquery script
<script id="todo-template" type="text/x-handlebars-template">
{{#this}}
<li {{#if completed}}class="completed"{{/if}} data-id="{{id}}">
<div class="view">
<input class="toggle" type="checkbox" {{#if completed}}checked{{/if}}>
<label>{{title}}</label>
<button class="destroy"></button>
</div>
<input class="edit" value="{{title}}">
</li>
{{/this}}
</script>
Since the id for each li is being set in the HTML markup, rather than by jQuery:
<li {{#if completed}}class="completed"{{/if}} data-id="{{id}}">
// ^^^^^^^^^^^^^^^^
Once you have a reference to the element in Javascript, all you need to do is retrieve the id property from the dataset, eg:
li.dataset.id
To do that, in your indexFromEl function, use:
const id = el.closest('li').dataset.id;
Or if you like using destructuring to make things a bit more DRY:
const { id } = el.closest('li').dataset;
Also note that it would be much cleaner to use findIndex if you want to find an index in an array:
indexFromEl: function (el) {
const { id } = el.closest('li').dataset;
return this.todos.findIndex(todo => todo.id === id);
}
(though, the above will return -1 if no index is found, rather than undefined, as your current code does, if that's an issue)
In your query version, you didn't return anything. So why returning JavaScript version?
First try to find what is this.todos[this.indexFromEl(el)] using instanceof
If this.todos[this.indexFromEl(el)] is Element then you can set(as you're assigning val) title attribute by setAttribute()
So,
this.todos[this.indexFromEl(el)].setAttribute('title', val);

How are the html and js codes separated from each other?

function show() {
var todos = get_todos();
var html = '<ul>';
for(var i=0; i<todos.length; i++) {
html += '<li>' + todos[i] + ' <button class="remove" id="' + i + '">Delete</button></li>';
};
html += '</ul>';
document.getElementById('todos').innerHTML = html;
var buttons = document.getElementsByClassName('remove');
for (var i=0; i < buttons.length; i++) {
buttons[i].addEventListener('click', remove);
};
}
In this code, I want to separate the 'ul and li' structure on the html page. Could you help me?
Here is want something you can do, if you want some code to html
You can refer the js from html using script tag check the plunker below
You need that script too, because you are adding li dynamically using script
Eg: <script src = "script.js"></script>
Steps:
I have taken a html and created a ul element.
Created a script which appends li to the ul element.
Added a reference to that script using the script tag
function show() {
var ul = document.getElementById('list');
var html = '';
var todos = ["Saab", "Volvo", "BMW"];
for(var i=0; i<todos.length; i++) {
var li = document.createElement("li");
li.appendChild(document.createTextNode(todos[i]));
ul.appendChild(li);
};
ul.append(html);
var buttons = document.getElementsByClassName('remove');
for (var i=0; i < buttons.length; i++) {
buttons[i].addEventListener('click', remove);
};
}
<!DOCTYPE html>
<html>
<body>
<ul id="list">
</ul>
<p id="demo" onClick="show()">Click ME</p>
</body>
</html>
Please run the above snippet
Here is a working demo for the same
I think what you want is an HTML template that you can re-use and keep separate from your JS code. A popular templating engine is handlebars.js.
The steps to get this to work are:
Write a template in your HTML. Using a hidden <div> is one way to do it.
Load the template contents in your JS code.
Compile the template to get a function which will accepts data and returns HTML.
Call that function with your data, assign the result to an existing container element.
Use event delegation to attach behavior to active elements like buttons.
Together this will look like this:
var todoListSource = document.getElementById("todoList").innerHTML;
var todoList = Handlebars.compile(todoListSource);
var items = ["Item 1", "Item 2", "Item 3", "Item 4"];
var todoContainer = document.getElementById("todo");
todoContainer.innerHTML = todoList(items);
// event delegation - use the same event handler for all buttons
// modified from https://stackoverflow.com/a/24117490/18771
todoContainer.addEventListener('click', function (e) {
if (!e.target.matches('.deleteTodo')) return;
e.stopPropagation();
var li = e.target.parentElement;
li.parentElement.removeChild(li);
});
.template {
display: none;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/handlebars.js/4.0.11/handlebars.min.js"></script>
<div id="todo"></div>
<div class="template" id="todoList">
<ul>
{{#each .}}
<li>
<span>{{.}}</span>
<button class="deleteTodo">delete</button>
</li>
{{/each}}
</ul>
</div>
Using jQuery you can simplify the JS part.
var todoList = Handlebars.compile($("#todoList").html());
var items = ["Item 1", "Item 2", "Item 3", "Item 4"];
$("#todo").html(todoList(items));
// event delegation - use the same event handler for all buttons
$("#todo").on('click', '.deleteTodo', function (e) {
e.stopPropagation();
$(this).closest('li').remove();
});
.template {
display: none;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/handlebars.js/4.0.11/handlebars.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div id="todo"></div>
<div class="template" id="todoList">
<ul>
{{#each .}}
<li>
<span>{{.}}</span>
<button class="deleteTodo">delete</button>
</li>
{{/each}}
</ul>
</div>

Simplify function for removing duplicate array

I want to find div element that contain custom attribute mod than append that div to list item. But first I have to remove divs that contain duplicate mod value. Here's what I have done
<div class="list"></div>
<div class="container">
<div mod="dog"></div>
<div mod="man"></div>
<div mod="woman"></div>
<div mod="dog"></div>
<div mod="bird"></div>
<div mod="insects"></div>
<div mod="dog"></div>
</div>
this is my script
modArr($('.container').find('[mod]'))
function modArr(el){
var filterArray = [] // store mod
, modNames = [] // store mod value
, arrIndex = [] // store non duplicate index
, li = [] // store
modArray = el
// store mod value
for(var i=0; i < modArray.length; i++){
modNames.push($(modArray[i]).attr('mod')) // get mod value from div
}
// search for non duplicate mod value and get the index of none duplicate mod
for(var i=0; i < modArray.length; i++){
if(filterArray.indexOf(modNames[i]) === -1){
filterArray.push(modNames[i])
arrIndex.push(i) // push non duplicate index value
}
}
filterArray = [] // reset filterArray
// push module from modArray to filterArray using index in arrIndex
for(var i=0; i < arrIndex.length; i++){
filterArray.push(modArray[arrIndex[i]])
}
// push to li array
$.each(filterArray,function(i,el){
li[i] = '<li>'+ el.outerHTML +'</li>'
})
$('<ul></ul>')
.append(li.join(''))
.appendTo('.list')
}
What you can see is that I've used to many loops, is there any simple way to do this. Thanks!
We can use an object as a map for checking duplicates, see comments (I've added text to the mod divs so we can see them):
modArr($('.container').find('[mod]'));
function modArr(elements) {
// A place to remember the mods we've seen
var knownMods = Object.create(null);
// Create the list
var ul = $("<ul></ul>");
// Loop the divs
elements.each(function() {
// Get this mod value
var mod = this.getAttribute("mod");
// Already have one?
if (!knownMods[mod]) {
// No, add it
knownMods[mod] = true;
ul.append($("<li></li>").append(this.cloneNode(true)));
}
});
// Put the list in the .list element
ul.appendTo(".list");
}
<div class="list"></div>
<div class="container">
<div mod="dog">dog</div>
<div mod="man">man</div>
<div mod="woman">woman</div>
<div mod="dog">dog</div>
<div mod="bird">bird</div>
<div mod="insects">insects</div>
<div mod="dog">dog</div>
</div>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
We can also just use the DOM to check for duplicates, but it's a bit slower (not that it matters for the number of elements here):
modArr($('.container').find('[mod]'));
function modArr(elements) {
// Create the list
var ul = $("<ul></ul>");
// Loop the divs
elements.each(function() {
// Get this mod value
var mod = this.getAttribute("mod");
// Already have one?
if (ul.find('div[mod="' + mod + '"]').length == 0) {
// No, add it
ul.append($("<li></li>").append(this.cloneNode(true)));
}
});
// Put the list in the .list element
ul.appendTo(".list");
}
<div class="list"></div>
<div class="container">
<div mod="dog">dog</div>
<div mod="man">man</div>
<div mod="woman">woman</div>
<div mod="dog">dog</div>
<div mod="bird">bird</div>
<div mod="insects">insects</div>
<div mod="dog">dog</div>
</div>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
Note: I used this.cloneNode(true) rather than outerHTML because there's no need to take a roundtrip through markup. If you want more jQuery there, it's $(this).clone(); ;-) Similarly, if you don't like this.getAttribute("mod"), there's $(this).attr("mod").
I'd be remiss if I didn't point out that mod is an invalid attribute name for div elements. You can use any name you want starting with data-, though, so perhaps use <div data-mod="dog"> instead.
Try this, only adds if an element with mod is not already in list:
$('.list').append('<ul>');
$('.container [mod]').each(function(index, el) {
if($('.list [mod=' + $(el).attr('mod') + ']').length === 0) {
$('.list ul').append($('<li>' + el.outerHTML + '</li>'));
}
});
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
<div class="list"></div>
<div class="container">
<div mod="dog">Dog1</div>
<div mod="man">Man1</div>
<div mod="woman">Woman1</div>
<div mod="dog">Dog2</div>
<div mod="bird">Bird1</div>
<div mod="insects">Insect1</div>
<div mod="dog">Dog3</div>
</div>

how to get elements in an element by attrbutes and update it node with purejs?

i have a html tag like this :
<div data-ctrl="master">
<div data-text="txtName"></div>
</div>
<div data-ctrl="master">
<div ><span data-text="txtName"></span> Wiratama</div>
</div>
and in js code i have :
var txtName = 'Yoza';
i want to insert html to every element in document with attribute data-text="txtName" in element with data-ctrl="master" with pure js.
function updateData(txtName){
var html = '<b>' + txtName + '</b>';
//update data for every element with attribute data-text txtName here
var allElements = document.getElementsByTagName('*');
for (var i = 0, n = allElements.length; i < n; i++) {
if (allElements[i].getAttribute("data-text") !== null) {
if (allElements[i].getAttribute("data-text") === 'txtName') {
console.log(nodeTemplate);
// allElements[i].innerHTML = html;
}
}
}
}
that is my js code i tried to.
Use querySelectorAll to return a list of nodes which match your selection. Then iterate over each of them, setting the innerHTML as you go:
function updateData(txtName){
var nodes = document.querySelectorAll('[data-ctrl="master"] [data-text="txtName"]');
for (i = 0; i < nodes.length; ++i) {
nodes[i].innerHTML = "<b>" + txtName + "</b>";
}
};
updateData("Yoza");
<div data-ctrl="master">
<div data-text="txtName"></div>
</div>
<div data-ctrl="master">
<div ><span data-text="txtName"></span> Wiratama</div>
</div>
On all modern browsers, and also IE8, you have querySelector and querySelectorAll on elements (and also on document), which accept CSS selectors. querySelector returns the first matching element (or null); querySelectorAll returns a list.
So if you have an element and want to find all of the elements within it that have the attribute data-text="txtName", you can do:
var list = theElementYouHave.querySelectorAll('[data-text="txtName"]');
So for example:
// Get some element
var element = document.getElementById("foo");
// Find all elements inside it that have `data-text` (at all)
snippet.log("Descendant elements with a data-text attribute: " +
element.querySelectorAll("[data-text]").length);
// Find the *first* element with `data-text="txtName"` and change its
// text to "Yoza":
var txtNameElement = element.querySelector('[data-text="txtName"]');
if (txtNameElement) {
txtNameElement.innerHTML = "Yoza";
}
<div id="foo">
<div data-ctrl="master">
<div data-text="txtName"></div>
</div>
<div data-ctrl="master">
<div><span data-text="txtName"></span> Wiratama</div>
</div>
</div>
<!-- Script provides the `snippet` object, see http://meta.stackexchange.com/a/242144/134069 -->
<script src="//tjcrowder.github.io/simple-snippets-console/snippet.js"></script>

Reference array element in jsView

I have simple test app, I want to remove and add tags, I have code like this:
<script id="tags_template" type="text/x-jsrender">
<div class="tags">
Tags:
<ul>
{^{for tags}}
<li>{{:name}}<a>×</a></li>
{{/for}}
<li><input /></li>
</ul>
</div>
</script>
and JS
var $view = $('#view');
var tags_tmpl = $.templates("#tags_template");
var tags = [];
tags_tmpl.link('#view', {tags: tags});
$view.find('input').keydown(function(e) {
if (e.which == 13) {
$.observable(tags).insert({name: $(this).val()});
$(this).val('');
}
});
$view.find('ul').on('click', 'a', function() {
// how to remove the tag?
});
Now how can I remove the tag? There is $.observable(array).remove but how can I reference that element in template and how can I get it in javascript?
Yes, your own answer is correct. But you may be interested in using a more data-driven and declarative approach, as follows:
<div id="view"></div>
<script id="items_template" type="text/x-jsrender">
Items (Hit Enter to add):
<ul>
{^{for items}}
<li>
{{:name}}
<a class="remove" data-link="{on ~remove}">×</a>
</li>
{{/for}}
</ul>
<input data-link="{:newName trigger=true:} {on 'keydown' ~insert}"/>
</script>
And
var items_tmpl = $.templates("#items_template");
var items = [];
items_tmpl.link('#view', {items: items}, {
insert: function(ev) {
if (ev.which === 13) {
// 'this' is the data item
$.observable(items).insert({name: this.newName});
$.observable(this).setProperty('newName', '');
}
},
remove: function() {
// 'this' is the data item
$.observable(items).remove($.inArray(this, items));
}
});
Alternatives for remove would be:
remove: function(ev) {
var view = $.view(ev.target);
$.observable(items).remove(view.index);
}
Or:
remove: function(ev, eventArgs) {
var view = $.view(eventArgs.linkCtx.elem);
$.observable(items).remove(view.index);
}
http://jsfiddle.net/BorisMoore/f90vn4mg/
BTW new documentation for {on ... event binding coming soon on http://jsviews.com
Found it in the docs:
$view.find('ul').on('click', 'a', function() {
var view = $.view(this);
$.observable(tags).remove(view.index);
});

Categories

Resources