Knockout multiple bindings on nested dom elements - javascript

I've managed to get myself into a bit of trouble with a project I'm working on.
Originally the site has one page on it that uses Knockout, with the other pages using jQuery. Due to some problems with the Foundation modal placing itself in the root of the body element, I ended up applying the bindings for the viewmodel for this page to the body element.
Fast forward 4 months, and without foreseeing the trouble I'm in now, I went and rebuilt our shopping basket in Knockout. The shopping basket is visible on every page and is included using a ZF2 partial.
Going back to the page I worked on 4 months ago, it is completely broken with the error message in console saying:
Uncaught Error: You cannot apply bindings multiple times to the same element.
Here's some code to show my layout:
<html>
<head>
<title>My Website</title>
</head>
<body> // 4 month old SPA bound here
<nav>
<div id='shopping-basket'> // Shopping basket bound here
...
</div>
</nav>
<div id='my-app'>
...
</div>
</body>
</html>
JavaScript:
var MyAppViewModel = function() {
// logic
};
var ShoppingBasketViewModel = function() {
//logic
};
ko.applyBindings(new MyAppViewModel(), document.body);
ko.applyBindings(new ShoppingBasketViewModel(), document.getElementById('shopping-basket');
If I had the time I could go back and rework the original application to sit within it's own div container that would site side by side with the basket, but unfortunately this isn't an option.
The other option is to discard the last bit of work I did on the shopping basket and replace it with jQuery, but this would mean losing a weeks worth of work.
Is there anyway when I'm applying the bindings that I could have both viewmodels working side by side, while being nested in the Dom, and remaining independent of each other?

I had some similar problem. I needed to applybinding to specific nested elements and start with a bind on the document first. Same problem. My solution was to add some ignore element part and than bind the specific element manually.
1) Add a custom binding so you can skip binding on the specific shopping basket:
ko.bindingHandlers.stopBinding = {
init: function() {
return { controlsDescendantBindings: true };
}
};
ko.virtualElements.allowedBindings.stopBinding = true;
2) Add the custom binding in your html (surround your shopping basket):
<html>
<head>
<title>My Website</title>
</head>
<body> // 4 month old SPA bound here
<nav>
<!-- ko stopBinding: true -->
<div id='shopping-basket'> // Shopping basket bound here
...
</div>
<!-- /ko -->
</nav>
<div id='my-app'>
...
</div>
</body>
3) Apply your bindings as you already do:
ko.applyBindings(new MyAppViewModel(), document.body);
ko.applyBindings(new ShoppingBasketViewModel(), document.getElementById('shopping-basket');
4) The first bind will skip the shopping-basket because of your custom binding handler and your second bind will explicitly bind the shopping-basket.
I haven't tested the code above on your specific example, but it should point you into the correct direction.

Related

Toggle Visibility of Separate Javascript Files on Same Wordpress Page

Let me preface by saying this is all in relation to a Wordpress page. My knowledge of JS is lacking at best and the concept of installing/loading/enqueueing a function on one area of the site and then calling that function in another area of the site is a something that makes sense to me in my head but is very new to me in practice and might need a little explaining.
I have two separate javascript files that I would like to load on a single page, but toggle visibility/display of either based on radio button input. The JS is provided by a 3rd party and is offsite. Their provided code is this:
<script src="https://toolkit.rescuegroups.org/j/3/FzemP6HU/toolkit.js"></script>
and
<script src="https://toolkit.rescuegroups.org/j/3/4ANRW3x8/toolkit.js"></script>
Each file presents a separate set of filtered results from their database. How can I incorporate both onto a page but only have one or the other showing based on a radio button form input? I would like the page to start off with nothing visible (hopefully giving time for both JS to load in the background while the user selects an option) and then show one or the other depending on what they selected.
You can see a single one of these in action at http://pricelesspetrescue.org/adoptable-dogs/. I'm trying to incorporate the use of an additional file on that same page based on input from the user and only showing one or the other rather than both.
I have tried to manage the following
<script src='http://ajax.googleapis.com/ajax/libs/jquery/1.10.2/jquery.min.js'></script>
<script type='text/javascript'>
function displayForm(c) {
if (c.value == "2") {
jQuery('#claremontdogContainer').toggle('show');
jQuery('#chdogContainer').hide();
}
if (c.value == "1") {
jQuery('#chdogContainer').toggle('show');
jQuery('#claremontdogContainer').hide();
}
};
</script>
<label>Please select a location to view:</label>
<form>
<input value="1" type="radio" name="formselector" onClick="displayForm(this)"></input>Chino Hills
<input value="2" type="radio" name="formselector" onClick="displayForm(this)"></input>Claremont
</form>
<div style="display:none" id="chdogContainer">
<script src="https://toolkit.rescuegroups.org/j/3/FzemP6HU/toolkit.js"></script>
<script type="text/javascript">
</script>
</div>
<!-- If I uncomment this second block the whole thing breaks
<div style="display:none" id="claremontdogContainer">
<script src="https://toolkit.rescuegroups.org/j/3/4ANRW3x8/toolkit.js"></script>
<script type="text/javascript">
</script>
</div>
-->
This gets pretty close to what I need. The problem I have is the second script load seems to conflict with the functions they provide in the script. It will display the initial result but does not carry any of the functionality that it should have. http://pricelesspetrescue.org/test-page/ Nothing is clickable inside those results and should be.
Been searching through various similar posts and the wordpress codex and...and...I just haven't been able to come up with anything that seems close enough to what I'm looking for to make the answer click in my head.
Edit: It seems that if I only load one of the scripts in either what I have above or the suggested answer below, all functionality is present when loaded. It's the loading of the second toolkit script that is breaking the page. I'm guessing one would need to be loaded then unloaded before loading the second for it to work. Any ideas?
The toolkit.js file you linked adds some common scripts to the DOM (via document.write function, which is not a good solution - see here: http://www.jameswiseman.com/blog/2011/03/31/jslint-messages-document-write-can-be-a-form-of-eval/), then populates an array (toolkitObjects) with a series of variables that are custom per file and finally loads some other scripts.
It also seems that each file loads a div with a specific class containing all the pets, and each div is identifiable by a specific class ( "rgtk-SOMEID" ) and therefore can be shown/hidden via javascript.
Here is an example of what you can obtain using the div class:
http://jsbin.com/loneyijuye/edit?html,output

inject content from one react component to another onClick

OK,so i am starting to get my head around ReactJs but keep getting stumped by one simple concept that is a doddle with plain old jQuery.
I want to be able to add to the content of one element on the screen when an on click event happens upon another. Following the react tutorial i completely understand the way they have achieved the adding to the comments list, the comment list is a child of the parent which is setting the state.. but surely this cannot be the only way as it feels very rigid and inflexible.
Here is a simple mockup of what I am trying to explain. On click of the button, i want to inject new content into the div with id "newComments"..
JSBin: http://jsbin.com/vokufujupu/1/edit
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>JS Bin</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/0.13.3/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/0.13.3/JSXTransformer.js"></script>
</head>
<body>
<div id="content"></div>
<script type="text/jsx">
var InputBox = React.createClass({
clickHandler: function(){
//Append a new string to the end of the existing copy in the copy box
alert('after this alert the value of the button should be appended to the content of the div#newComments');
},
render: function() {
return (
<div classNmae="copyBox">
<input type="button" name="inputButton" value="click me button" className="bbbc"
onClick={this.clickHandler} />
</div>
);
}
});
var CopyBox = React.createClass({
render: function() {
return (
<div classNmae="copyBox">
<p>div#newComments:</p>
<div id="newComments"></div>
</div>
);
}
});
var Page = React.createClass({
render: function() {
return (
<div>
<CopyBox/>
<InputBox/>
</div>
);
}
});
React.render(
<Page/>,
document.getElementById('content')
);
</script>
<!-- The equiv in plain old js -->
<script type="text/javascript">
function newContent(obj){
document.getElementById('vanBox').innerHTML = document.getElementById('vanBox').innerHTML + obj.value;
}
</script>
<div id="vanilaJsEquiv">
<div id="vanBox"></div>
<input type="button" value="ClickyClik" onclick="newContent(this)"/>
</div>
</body>
</html>
I've been hunting around google and the docs for yonks and cannot find the answer..
In react there is no concept of manipulating HTML / DOM. React is responsible just for rendering based on component state. Every component renders whatever it's current state is.
So you need to manipulate the state of other component. For that Facebook is using Flux. Which is a bit more complex workflow, but once you get it, it is actually pretty simple concept.
On one component click you dispatch an action. That action will trigger event, stores that are subscribed to that event will react and update internal state. After update, store emits change event, all components listening for changes in that store will update.
Yes you will need to write a lot more code. It gets much simpler if component is manipulating it's own state, then it would be enough to just call this.setState({ ... }) inside the component. And yes there, are other ways to do this.

Javascript inside Mustache.js template

I have a list of users coming from back-end and I append each one of them to my HTML page like below. My goal is to have javascript rating system for every user.
<head>
<link href="/css/rateyo.css"/>
<script src="/js/rateyo.js"/>
<script type="text/template" id="mustache-template">
{{#user}}
<li>
{{name}}
<div id="rating"></div>
</li>
{{/user}}
</script>
<script>
$("#rating").rateYo().on("rateyo.set", function (e, data) {
});
</script>
</head>
<body>
<ul>
<!-- All the individual users will be in their own li element here -->
</ul>
</body>
Everything is working except my rating script. It should make five stars next to each user. But I heard that you can't put scripts inside templates, is it correct? If I move that <div id="rating"/> to somewhere else it works like it's meant and shows the stars.
What should I do? I can't really put that script outside of my templates.
You can add your function to the object you are trying to parse with the template, and then call the function in your template.
var userlist = {
user: [
{name: 'name nameson', doRating: function() {someOtherFunction();}},
{name: 'some otherguy', doRating: function() {someOtherFunction();}}
]
};
var someOtherFunction = function() {
$("#rating").rateYo().on("rateyo.set", function (e, data) {
//Do stuff?
});
}
<script type="text/template" id="mustache-template">
{{#user}}<li>{{name}} <div id="rating"></div></li>{{/user}}{{doRating}};
</script>
Or you could of course calculate the rating on beforehand, and implement some sort of {{user.rating}} and use that to generate in your template.
Or as I look closer, you could just add the listener after mustache is done rendering. Since the div#rating would then be available (which should really be a class btw).
This would be my approach at least.

jQuery Within a PHP While Loop

I am using jQuery to reveal an extra area of a page when a button is clicked.
The script is
$(document).ready(function() {
$("#prices").on('click', 'a.click', function() {
$(".hiddenstuff").slideToggle(1000),
$("a.click").toggleClass("faded");
});
});
Then the button is
Enquire or Book
and the newly revealed area is
<div class="hiddenstuff" style="display:none">
<!-- HTML form in here -->
</div>
The problem I have is that the button and "hiddenstuff" div are wrapped in a PHP while loop so they repeat anything between one and six times. When the user clicks on one of the buttons, all the hidden divs are revealed. I would like just the hidden div related to the clicked button to reveal.
I presume that I have to create a javascript variable that increments in the while loop and somehow build that into the script. But I just can't see how to get it working.
EDIT, in response to the comments
The while loop is actually a do-while loop. The code inside the loop is about 200 lines of PHP and HTML. That's why I didn't show it all in my question. In a shortened version, but not as shortened as before, it is
do {
<!-- HTML table in here -->
Enquire or Book
<!-- HTML table in here -->
<div class="hiddenstuff" style="display:none">
<!-- HTML form and table in here -->
</div>
<!-- More HTML in here -->
} while ($row_season = mysql_fetch_assoc($season));
EDIT 2
The final solution was exactly as in UPDATE2 in the reply below.
The easiest thing for you to do is to keep your onclick binding but change your hiddenstuff select. Rather than grabbing all the hiddenstuffs which you are doing now, you can search for the next one [the element directly after the specific button that was clicked].
$(this).next('div.hiddenstuff').slideToggle(1000);
UPDATE
i created a fiddle for you with what I would assume would be similar to the output from your php loop. one change from my early answer was rather than using next(), i put a div around each group as I would assume you would have and used .parent().find()
http://jsfiddle.net/wnewby/B25TE/
UPDATE 2: using IDs
Seeing your PHP loop and your nested tables and potentially complex html structure, I no longer thing jquery select by proximity is a good idea [be it by big parent() chains or sibling finds].
So I think this is a case for injecting your ids. I assume your table structure has an id that you can get from $row_season ( $row_season["id"] )
you can then place it in the anchor:
Enquire or Book
and the same for your hiddenstuff
<div class="hiddenstuff" data-rowid=" . $row_season['id'] . " style="display:none">
and then your js can find it easily
$(document).ready(function() {
$("#prices").on('click', 'a.click', function() {
var rowid = $(this).attr("data-rowid");
$(".hiddenstuff[data-rowid='" + rowid + "']").slideToggle(1000),
$(this).toggleClass("faded");
});
});
updated fiddle
If your structure is something like this:
<div class="container">
Enquire or Book
<div class="hiddenstuff" style="display:none">
<!-- HTML form in here -->
</div>
</div>
You can do your js like this:
$(document).ready(function() {
$("#prices").on('click', 'a.click', function() {
$(this).siblings(".hiddenstuff").slideToggle(1000),
$(this).toggleClass("faded");
});
});
which is similar to William Newby answer, but a close look at your while loop, I'd think you could do this:
$(document).ready(function() {
$("#prices").on('click', 'a.click', function() {
var index = $(this).index();
$(".hiddenstuff")[index].slideToggle(1000),
$(this).toggleClass("faded");
});
});
There are several ways of do it, I hope I was useful.

Knockout + Ajax Content results in multiple binding error

I have built a web application with multiple pages. Some of them are Knockout-driven.
I am trying to apply some Ajax-optimized page loading and stumble over the following issue.
Say I have the following general page structure
<body>
<div id="content">
</div>
</body>
And the following view, which is using Knockout. I include the call to applyBindings inline for being able to load the right ViewModel for every view.
<section id="editor">
<ul data-bind="foreach: items">
....
</form>
</section>
<script>
ko.applyBindings({items: {}}, $("#editor").el)
</script>
I load the view asynchronously into div#content for example using JQuery.load("editor.html #content")
The first page load works fine, but when navigating away (again using JQuery.load) from this view and coming back again I receive the error:
You cannot apply bindings multiple times to the same element.
I have already tried to apply ko.cleanNode but with no success. What am I missing? The #editor node should be removed from the DOM when other content is shown. So I really do not understand how to clean bindings or reinitialize knockout.
Note: I do not want the old data, I want to initialize the Bindings like on a freshly loaded page
Could you test your $("#editor").el in console? It doesn't work in standard jQuery.
If your $("#editor").el returns undefined, your ko.applyBindings({items: {}}, $("#editor").el) is essentially binding to window.document.body.
You may try
ko.applyBindings({items: {}}, $("#editor").get(0));
...
// call cleanNode before loading new page.
ko.cleanNode($("#editor").get(0));
$("#content").load( "newpage.html" );
if your bindings in "editor" section doesn't change,i suggest you to load(AJAX) only json data from server,and replace(modify) your viewModel in the browser,in that way knockout will refresh the dom automaticly.

Categories