Can't easily access and re-render elements from within jQuery object - javascript

I'm trying to render a partial and rerender elements of my page with updated elements from said partial. My js with erb as of right now looks like this (which I'm not happy with):
var showPage = '<%= j render partial: "orders/show-price", locals: {variant: #variant, line_item: #line_item} %>'
$('[data-unit-price]').html($(showPage)[0])
$('[data-total-price]').html($(showPage)[2])
showPage returns a string of HTML:
<p data-unit-price="">Unit Price: 11.86</p>
<p data-total-price="">Total: 118.60</p>
And $(showPage) returns T.fn.init(3) [p, text, p]
Ordinarily, I'd just do something like:
$('[data-unit-price]').html($('[data-unit-price]', showPage))
which in fact exists similarly elsewhere in my code:
var sidebar = '<%= j render partial: "layouts/sidebar" %>'
if($('[data-cart-link]').length){
$('[data-cart-link]').html($('[data-cart-link]', sidebar))
} else {
$('[data-cart-link-prepend-target]').prepend($('[data-cart-link]', sidebar))
}
where sidebar also returns an html string. The above example works as desired in that it takes updated information from a rendered partial and rerenders that element in the DOM.
What am I doing wrong here? What is different about the two situations? I want to be able to find my elements by attribute rather than by array index, but I'm not having any luck.
Thanks very much.

The issue is because when you provide a selector a context to search within, as you are in this line: $('[data-cart-link]', sidebar), it uses find() internally. This will not work for the HTML you have stored in showPage because there is no parent element to find() within, hence nothing is returned.
The solution to this problem is to instead use filter() to search through all elements in the current collection:
var showPage = `<p data-unit-price="">Unit Price: 11.86</p>
<p data-total-price="">Total: 118.60</p>`;
var $showPage = $(showPage);
$('[data-unit-price]').append($showPage.filter('[data-unit-price]'));
$('[data-total-price]').append($showPage.filter('[data-total-price]'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div data-unit-price></div>
<div data-total-price></div>
Also note the use of append() here, as you're providing a jQuery object to the method, not a string.

Related

show all the values with .html [duplicate]

Lets say I have an empty div:
<div id='myDiv'></div>
Is this:
$('#myDiv').html("<div id='mySecondDiv'></div>");
The same as:
var mySecondDiv=$("<div id='mySecondDiv'></div>");
$('#myDiv').append(mySecondDiv);
Whenever you pass a string of HTML to any of jQuery's methods, this is what happens:
A temporary element is created, let's call it x. x's innerHTML is set to the string of HTML that you've passed. Then jQuery will transfer each of the produced nodes (that is, x's childNodes) over to a newly created document fragment, which it will then cache for next time. It will then return the fragment's childNodes as a fresh DOM collection.
Note that it's actually a lot more complicated than that, as jQuery does a bunch of cross-browser checks and various other optimisations. E.g. if you pass just <div></div> to jQuery(), jQuery will take a shortcut and simply do document.createElement('div').
EDIT: To see the sheer quantity of checks that jQuery performs, have a look here, here and here.
innerHTML is generally the faster approach, although don't let that govern what you do all the time. jQuery's approach isn't quite as simple as element.innerHTML = ... -- as I mentioned, there are a bunch of checks and optimisations occurring.
The correct technique depends heavily on the situation. If you want to create a large number of identical elements, then the last thing you want to do is create a massive loop, creating a new jQuery object on every iteration. E.g. the quickest way to create 100 divs with jQuery:
jQuery(Array(101).join('<div></div>'));
There are also issues of readability and maintenance to take into account.
This:
$('<div id="' + someID + '" class="foobar">' + content + '</div>');
... is a lot harder to maintain than this:
$('<div/>', {
id: someID,
className: 'foobar',
html: content
});
They are not the same. The first one replaces the HTML without creating another jQuery object first. The second creates an additional jQuery wrapper for the second div, then appends it to the first.
One jQuery Wrapper (per example):
$("#myDiv").html('<div id="mySecondDiv"></div>');
$("#myDiv").append('<div id="mySecondDiv"></div>');
Two jQuery Wrappers (per example):
var mySecondDiv=$('<div id="mySecondDiv"></div>');
$('#myDiv').html(mySecondDiv);
var mySecondDiv=$('<div id="mySecondDiv"></div>');
$('#myDiv').append(mySecondDiv);
You have a few different use cases going on. If you want to replace the content, .html is a great call since its the equivalent of innerHTML = "...". However, if you just want to append content, the extra $() wrapper set is unneeded.
Only use two wrappers if you need to manipulate the added div later on. Even in that case, you still might only need to use one:
var mySecondDiv = $("<div id='mySecondDiv'></div>").appendTo("#myDiv");
// other code here
mySecondDiv.hide();
if by .add you mean .append, then the result is the same if #myDiv is empty.
is the performance the same? dont know.
.html(x) ends up doing the same thing as .empty().append(x)
Well, .html() uses .innerHTML which is faster than DOM creation.
.html() will replace everything.
.append() will just append at the end.
You can get the second method to achieve the same effect by:
var mySecondDiv = $('<div></div>');
$(mySecondDiv).find('div').attr('id', 'mySecondDiv');
$('#myDiv').append(mySecondDiv);
Luca mentioned that html() just inserts hte HTML which results in faster performance.
In some occassions though, you would opt for the second option, consider:
// Clumsy string concat, error prone
$('#myDiv').html("<div style='width:'" + myWidth + "'px'>Lorem ipsum</div>");
// Isn't this a lot cleaner? (though longer)
var newDiv = $('<div></div>');
$(newDiv).find('div').css('width', myWidth);
$('#myDiv').append(newDiv);
Other than the given answers, in the case that you have something like this:
<div id="test">
<input type="file" name="file0" onchange="changed()">
</div>
<script type="text/javascript">
var isAllowed = true;
function changed()
{
if (isAllowed)
{
var tmpHTML = $('#test').html();
tmpHTML += "<input type=\"file\" name=\"file1\" onchange=\"changed()\">";
$('#test').html(tmpHTML);
isAllowed = false;
}
}
</script>
meaning that you want to automatically add one more file upload if any files were uploaded, the mentioned code will not work, because after the file is uploaded, the first file-upload element will be recreated and therefore the uploaded file will be wiped from it. You should use .append() instead:
function changed()
{
if (isAllowed)
{
var tmpHTML = "<input type=\"file\" name=\"file1\" onchange=\"changed()\">";
$('#test').append(tmpHTML);
isAllowed = false;
}
}
This has happened to me . Jquery version : 3.3.
If you are looping through a list of objects, and want to add each object as a child of some parent dom element, then .html and .append will behave very different. .html will end up adding only the last object to the parent element, whereas .append will add all the list objects as children of the parent element.

jQuery selector is overlooking elements from a separate context

I'm making an ajax request to a page on my site with this element as a direct child of the body tag:
<div class="container" id="wantme"><div class="content"></div></div>
There's only one .container, and I want to grab its ID which I don't know.
As far as I can tell, this code should do what I want:
$.get('/page', function(data) {
id = $('.container', data).attr('id');
});
But the .container selector fails to find anything.
I did find these two workarounds. I can find .content, and I can climb up the tree like this:
id = $('.content', data).parent().attr('id');
But I can't leap directly there.
I found this workaround elsewhere on StackOverflow that works:
html = $('<div></div>').html(data);
id = html.find('.container').attr('id');
But why is it that the seemingly obvious answer doesn't work?
UPDATED ANSWER: I'll leave my original answer at the bottom, however I'm concerned it may misbehave depending on browser. jQuery's .html() makes use of Javascript's innerHTML - some browsers choose to strip <head> and <body> tags when using innerHTML, whereas others do not.
The safest method to achieve what you're after may still be the workaround you mentioned, like so:
var data = '<!doctype html><html><body><div class="container" id="findme"><div class="content"></div></div></body></html>';
var $container = $("<div />").html(data).find(".container");
var id = $container.attr("id");
console.log(id);
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
More information as to the browser-related issues can be found here.
PREVIOUS ANSWER:
When you pass HTML to a jQuery element, it will ignore the <body> tags, as well as anything outside of them. Given the data string in your JSFiddle, $(data) will create something that looks like this:
<div class="container" id="findme">
<div class="content"></div>
</div>
As you can see in the HTML above, your .container isn't inside of $(data) - it is $(data).
Because $(data) is representing your .container element, you should just be able to do $(data).attr("id") to retrieve what you're after.
var data = '<!doctype html><html><body><div class="container" id="findme"><div class="content"></div></div></body></html>';
var id = $(data).attr('id');
console.log(id);
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
You are not getting the ID from $('.container', data).attr('id'); is because you are setting the value of the second parameter. What you want to do is this: $('.container ' + data).attr('id');.
Update:
If data is a string then you should convert it into a DOM element: $('.container', $(data)).attr('id');

jQuery "add" Only Evaluated When "appendTo" Called

this has been driving me crazy since yesterday afternoon. I am trying to concatenate two bodies of selected HTML using jQuery's "add" method. I am obviously missing something fundamental. Here's some sample code that illustrated the problem:
<html>
<head>
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.7.1/jquery.min.js"></script>
</head>
<body>
<p id="para1">This is a test.</p>
<p id="para2">This is also a test.</p>
<script>
var para1 = $("#para1").clone();
var para2 = $("#para2").clone();
var para3 = para1.add(para2);
alert("Joined para: " + para3.html());
para3.appendTo('body');
</script>
</body>
</html>
I need to do some more manipulation to "para3" before the append, but the alert above displays only the contents of "para1." However, the "appendTo appends the correct, "added" content of para1 and para2 (which subsequently appears on the page).
Any ideas what's going on here?
As per the $.add,
Create a new jQuery object with elements added to the set of matched elements.
Thus, after the add, $para3 represents a jQuery result set of two elements ~> [$para1, $para2]. Then, per $.html,
Get the HTML contents of the first element in the set of matched elements or set the HTML contents of every matched element.
So the HTML content of the first item in the jQuery result ($para1) is returned and subsequent elements (including $para2) are ignored. This behavior is consistent across jQuery "value reading" functions.
Reading $.appendTo will explain how it works differently from $.html.
A simple map and array-concat can be used to get the HTML of "all items in the result set":
$.map($para3, function (e) { return $(e).html() }).join("")
Array.prototype.map.call($para3, function (e) { return $(e).html() }).join("")
Or in this case, just:
$para1.html() + $para2.html()
Another approach would be to get the inner HTML of a parent Element, after the children have been added.

Turning off div wrap for Backbone.Marionette.ItemView

I'm looking at the Angry Cats Backbone/Marionette tutorial posts here
http://davidsulc.com/blog/2012/04/15/a-simple-backbone-marionette-tutorial/
http://davidsulc.com/blog/2012/04/22/a-simple-backbone-marionette-tutorial-part-2/
and I came upon the same question/need posted here:
Backbone.js turning off wrap by div in render
But I can only get that to work for Backbone.Views, not Backbone.Marionette.ItemViews.
For example, from the simple backbone marionette tutorial links above, take AngryCatView:
AngryCatView = Backbone.Marionette.ItemView.extend({
template: "#angry_cat-template",
tagName: 'tr',
className: 'angry_cat',
...
});
The template, #angry_cat-template, looks like this:
<script type="text/template" id="angry_cat-template">
<td><%= rank %></td>
<td><%= votes %></td>
<td><%= name %></td>
...
</script>
What I don't like, is that the AngryCatView needs to have
tagName: 'tr',
className: 'angry_cat',
-- if I take tagName out, then angry_cat-template gets wrapped by a <div>.
What I would like is to specify the HTML in one place (the angry_cat-template) and not have most HTML (all the <td> tags) in angry_cat-template and a little HTML (the <tr> tag) in AngryCatView. I would like to write this in angry_cat-template:
<script type="text/template" id="angry_cat-template">
<tr class="angry_cat">
<td><%= rank %></td>
<td><%= votes %></td>
<td><%= name %></td>
...
</tr>
</script>
It just feels cleaner to me but I've been mucking around with Derik Bailey's answer in "Backbone.js turning off wrap by div in render" and can't get it to work for Backbone.Marionette.
Any ideas?
2014/02/18 — updated to accommodate the improvements noted by #vaughan and #Thom-Nichols in the comments
In many of my itemView/layouts I do this:
var Layout = Backbone.Marionette.Layout.extend({
...
onRender: function () {
// Get rid of that pesky wrapping-div.
// Assumes 1 child element present in template.
this.$el = this.$el.children();
// Unwrap the element to prevent infinitely
// nesting elements during re-render.
this.$el.unwrap();
this.setElement(this.$el);
}
...
});
The above code only works when the wrapper div contains a single element, which is how I design my templates.
In your case .children() will return <tr class="angry_cat">, so this should work perfect.
I agree, it does keep the templates much cleaner.
One thing to note:
This technique does not force only 1 child element. It blindly grabs .children() so if you've incorrectly built the template to return more than one element, like the first template example with 3 <td> elements, it won't work well.
It requires your template to return a single element, as you have in the second template with the root <tr> element.
Of course it could be written to test for this if need be.
Here is a working example for the curious: http://codepen.io/somethingkindawierd/pen/txnpE
While I'm sure there's a way to hack the internals of render to get it to behave the way you'd like, taking this approach means you'll be fighting the conventions of Backbone and Marionette through the whole development process. ItemView needs to have an associated $el, and by convention, it's a div unless you specify a tagName.
I empathize -- especially in the case of Layouts and Regions, it appears to be impossible to stop Backbone from generating an extra element. I'd recommend accepting the convention while you learn the rest of the framework and only then deciding if it's worth hacking render to behave differently (or to just choose a different framework).
This solution works for re-rendering. You need to override render.
onRender tricks won't work for re-render. They will cause nesting on every re-render.
BM.ItemView::render = ->
#isClosed = false
#triggerMethod "before:render", this
#triggerMethod "item:before:render", this
data = #serializeData()
data = #mixinTemplateHelpers(data)
template = #getTemplate()
html = Marionette.Renderer.render(template, data)
##$el.html html
$newEl = $ html
#$el.replaceWith $newEl
#setElement $newEl
#bindUIElements()
#triggerMethod "render", this
#triggerMethod "item:rendered", this
this
Wouldn't it be cleaner to use vanilla JS instead of jQuery to accomplish this?
var Layout = Backbone.Marionette.LayoutView.extend({
...
onRender: function () {
this.setElement(this.el.innerHTML);
}
...
});
For IE9+ you could just use firstElementChild and childElementCount:
var Layout = Backbone.Marionette.LayoutView.extend({
...
onRender: function () {
if (this.el.childElementCount == 1) {
this.setElement(this.el.firstElementChild);
}
}
...
});
There is a good reason why Marionette automatically inserts the wrapper DIV. It's only when your template consists of just one element when you can drop it. Hence the test for number of child elements.
Another option is to use the attachElContent method present in every Marionette view. Its default implementation means re-renders of the view will overwrite the root element's inner HTML. This ultimately gives rise to the infinite nesting mentioned in bejonbee's answer.
If you would rather not overwrite onRender and/or require a pure-JS solution, the following code might be just what you want:
var Layout = Backbone.Marionette.LayoutView.extend({
...
attachElContent: function (html) {
var parentEl = this.el.parentElement;
var oldEl;
//View already attached to the DOM => re-render case => prevents
//recursive nesting by considering template's top element as the
//view's when re-rendering
if (parentEl) {
oldEl = this.el;
this.setElement(html); //gets new element from parsed html
parentEl.replaceChild(this.el, oldEl); //updates the dom with the new element
return this;
//View hasn't been attached to the DOM yet => first render
// => gets rid of wrapper DIV if only one child
} else {
Marionette.ItemView.prototype.attachElContent.call(this, html);
if (this.el.childElementCount == 1) {
this.setElement(this.el.firstElementChild);
}
return this;
}
}
...
});
Note that for re-rendering to work, the code assumes a template with a single child that contains all markup.

backbone view with dynamic id

I just realized I was misunderstanding the el attribute of a Backbone.View. Basically my views require dynamic id attributes based on its model's attribute. I thought I had this working fine because I simply specified it in my template:
<script type="text/template" id="item_template">
<li class="item" id="{{identifier}}">
<span class="name">{{name}}</span>
</li>
</script>
However, I realized that what Backbone was actually doing was putting this compiled template into another element, div by default. I learned more about this by reading the documentation, but I'm still confused on how to create a dynamic id.
Preferably, I would love to find a way to make it such that the stuff in the above template serves as my el, since it already has everything I want, but I don't know if that is possible. So I'm wondering if, quite simply, there is a way to specify a dynamic id attribute.
I tried setting it within the initialize method, this.id = this.model.get('attr') but it didn't seem to have any effect, possibly because by this time it is already too late.
What I'm currently doing is just using jQuery to add the id in during render():
this.el.attr(id: this.model.get('identifier'));
it works, but of course, I'm simply asking if there is a preferred way to do it through Backbone.
Yes there is a standard way to do this in Backbone. You can pass id to the View constructor. You can also refactor your template so that Backbone creates the parent <li> element for you. Try this simpler template:
<script type="text/template" id="item_template">
<span class="name">{{name}}</span>
</script>
And add these to your view:
myView = Backbone.View.extend({
className: "item",
tagName: "li"
})
And instantiate it like this:
var view = new YourView({
model: mymodel,
id: mymodel.get('identifier') // or whatever
})
Good luck!
There is one more approach. I found it more convenient than passing id every time you create an instance of your view.
Template:
<script type="text/template" id="item_template">
<span class="name">{{name}}</span>
</script>
View:
var MyView = Backbone.View.extend({
tagName: 'li',
attributes: function(){
return {
id: this.model.get('identifier'),
class: 'item'//optionally, you could define it statically like before
}
}
})
When you create your view, pass in a selector that will let the view find your existing pre-rendered DOM element:
var id = "1234";
var view = YourView({el: '#'+id});

Categories

Resources