First, a little disclaimer: I'm no master at working with javaScript / jQuery, although I have handled it quite a few times. Recently, I have sometimes found myself being forced to make redundant code, i.e. repeat line(s) of code regarding diferent events, because I don't know how to do it in a more efficient way. A small example would be enabling a button after checking if any checkbox in a page is selected. This is done either upon page loading or after any checkbox is selected:
var checkboxes = $("input[type='checkbox']");
$('#nextfl').attr("disabled", !checkboxes.is(":checked"));
checkboxes.click(function()
{
$('#nextfl').attr("disabled", !checkboxes.is(":checked"));
});
I don't think this could be solved using the bind or on functions, for instance, since it refers to events not related to the same element. I believe it must exist a straightforward solution to this though, but as I said before, I have little experience in JS / jQ, and there are some similar situations where I have repeated dozens of lines of code, which is of course at least a bad practice.
You can always split redundant code into functions with javascript:
function doCheckboxLogic () {
$('#nextfl').attr("disabled", !checkboxes.is(":checked"));
// and any other logic that needs to be done
}
You then call that function in place of the redundant code block:
checkboxes.click(function()
{
doCheckboxLogic();
});
There's not much gained here since it's one line of code anyway, but this really helps with encapsulating more complicated blocks of logic
Best practice in this case is to extract common logic to its own function which can be called as required. Try this:
$(function() {
var $checkboxes = $("input[type='checkbox']");
function checkState() {
$('#nextfl').attr("disabled", !$checkboxes.is(":checked"));
}
$checkboxes.click(checkState); // run the function on click of a checkbox
checkState(); // run it on load of the page
});
Related
I apologize if this is a duplicate, just haven't been able to find anything close to this myself.
The company I work for has an online reporting system that is run by an ng-app applied directly to the body tag. I have been tasked with modifying the result that returns from this ng-app. Following code is called using onload attached to the body tag.
function getElements(){
var list;
list = document.getElementsByClassName("neutral");
[].forEach.call(list, function (listItem) {
addNeutral(listItem);
});
...
Basically, trying to find anything with class "neutral" and apply results from another function to it. The addNeutral function is basically just
element.classList.add("neutralHighlight");
This code seems to run and gathers the correct list of elements, but the new class is never added and no errors occur. So long story short, is there any way to modify the output of a ng-app using code separate from the ng-app itself? Any guidance would be greatly appreciated.
Update 3/5/20
So I implemented Shaun's response and it still isn't working properly. With some debug messages, I can see that it collects the "list" variable as an HTMLCollection. The forEach function doesn't seem to even trig
function getElements(){
var list;
list = document.getElementsByClassName("neutral");
console.log(list); //Debug - Shows in console
[].forEach.call(list, function (listItem) {
console.log(listItem); //Debug - Does not show in console
addNeutral(listItem);
});
}
function addNeutral(element){
angular.element(element).addClass("neutralHighlight");
console.log("!!!end addNeutral"); //Debug - Does not show in console
}
Update 3/9/20 -SOLUTION-
Application is returning the HTML Collection, but it displays with a length of 0 (still displays the objects, but I think that's a Firefox console thing). When trying to loop through the list items, it returns null for the first item, so the function is still being called before the Angular app loads completely.
That being said, I messed around with things a bit this morning and came to a solution! I ended up using the setInterval function with a 5 second interval (since I need it to update, I may change this to optimize it later by adding onChange items to the objects I grab initially). The setTimeout that was proposed would have worked with a delay added to it. This probably isn't the most elegant solution, and there's probably a better way to do it, but this works for anyone interested.
function getElements(){
var list;
list = document.getElementsByClassName("neutral");
for (i = 0; i <= list.length; i++){
var listItem = list.item(i);
addNeutral(listItem);
}
}
function loadFunction(){
setInterval(function(){getElements()}, 5000);
}
I added <script>loadFunction()</script> right before the closing HTML tag to execute.
Update 4/21/20 -IMPROVED SOLUTION- CSS Attributes
So this is a bit specific to my scenario, but I wanted to include it for anybody else who may come across this in the future. I was actually able to accomplish this entirely with CSS attribute selectors. The tags that I wanted to edit all had specific titles assigned to them via the ng-app provided from the company, so I was able to use CSS selectors like div [title~="NotReadyForNextCall"]{<custom styling here>} to select any block that included an agent who was not ready for their next call. This is a much better solution in terms of resources required to operate and overall performance, so I hope it helps anybody looking at this down the line!
You might be able to get around this by using the angular object in your code and adding the class on an angular.element instead. AngularJS doesn't use a virtual DOM but it does use its own node references (which is what makes it so tricky to work with outside of the framework, as Lex pointed out in the comments of your question). Try:
angular.element(element).addClass("neutralHighlight");
Yes, you have access to angular outside of the app! And a last note, addClass() is available on angular.element because AngularJS comes with jqLite.
Further investigation
It looks like the above solution works if the class 'neutral' is being added in angular via the class attribute, but it looks like your app may be adding it programmatically with the ng-class directive after the DOM has rendered.
I wrapped your getElements() function in a setTimeout():
setTimeout(getElements);
This is unfortunately not a guarantee that the ng-class update will have taken place, but what it does is it executes the function after the previous digest cycle has completed and that appears to be working.
An even safer solution would be to use document.ready but again with the angular.element wrapper. This will ensure the initial DOM state has been rendered by AngularJS, including applied directives:
angular.element(document).ready(function() {
getElements();
});
EDIT: Update 3/9/20 -SOLUTION-
The solution proposed in the answer is almost identical to the setTimeout() answer given here. The only difference is setInterval() will keep executing the code every 5 seconds until you tell it to stop.
You can do this with the following:
var loadFunction = setInterval(function() {
var el = getElements();
if (el) clearInterval(loadFunction);
}, 5000);
And just return a bool in your getElements() like so:
function getElements() {
var list;
var found = false;
list = document.getElementsByClassName("neutral");
[].forEach.call(list, function (listItem) {
addNeutral(listItem);
found = true;
});
return found;
}
See: codepen.io/shaunetobias/pen/KKpXRxq
I am trying to setup a CasperJS script that will do some testing on a personal site.
I need to check for a selector, and if it cannot be found on the page, click the next page link in the pagination and check again.
I am struggling to wrap my head around this problem and how to solve it. I know I need some sort of loop, and I even tried a while() loop, but I don't understand CasperJS enough to get it working.
Basic idea of what I want, in psuedocode:
open page http://www.example.com
check if 'li.my-class' exists
if not
click '.next-page'
then check again for 'li.my-class'
(repeat this process)
else
'li.my-class' exists, go do something else
I've tried reading about waitFor, waitforSelector etc. The documentation doesn't help me much as it's quite basic in terms of examples.
I recommend you to use recursion, especially a recursive IIFE. Here is the implementation:
var casper = require('casper').create();
casper.start('http://www.example.com');
(function go() {
casper.wait(1000, function () {
if (!this.exists('li.my-class')) {
this.click('.next-page');
go();
} else {
// Do something...
}
});
})();
casper.run();
I have been writing a reusable script, let's call it a plugin although it's not jQuery, that can be initialised in a declarative way from the HTML. I have extremely simplified it to explain my question so let's say that if a user inserts a tag like:
<span data-color="green"></span>
the script will fire because the attribute data-color is found, changing the color accordingly.
This approach proved very handy because it avoids anyone using the plugin having to initialise it imperatively in their own scripts with something like:
var elem = document.getElementsByTagName('span')[0];
myPlugin.init(elem);
Moreover by going the declarative way I could get away without defining any global (in this case myPlugin), which seemed to be a nice side effect.
I simplified this situation in an example fiddle here, and as you can see a user can avoid writing any js, leaving the configuration to the HTML.
Current situation
The plugin is wrapped in a closure like so:
;(function(){
var changeColor = {
init : function(elem){
var bg = elem.getAttribute('data-color');
elem.style.background = bg;
}
};
// the plugin itslef looks for appropriate HTML elements
var elem = document.querySelectorAll('[data-color]')[0];
// it inits itself as soon as it is evaluated at page load
changeColor.init(elem);
})();
The page loads and the span gets the correct colour, so everything is fine.
The problem
What has come up lately, though, is the need to let the user re-evaluate/re-init the plugin when he needs to.
Let's say that in the first example the HTML is changed dynamically after the page is loaded, becoming:
<span data-color="purple"></span>
With the first fiddle there's no way to re-init the plugin, so I am now testing some solutions.
Possible solutions
Exposing a global
The most obvious is exposing a global. If we go this route the fiddle becomes
http://jsfiddle.net/gleezer/089om9z5/4/
where the only real difference is removing the selection of the element, leaving it to the user:
// we remove this line
// var elem = document.querySelectorAll('[data-color]')[0];
and adding something like (again, i am simplifying for the sake of the question):
window.changeColor = changeColor;
to the above code in order to expose the init method to be called from anywhere.
Although this works I am not satisfied with it. I am really looking for an alternative solution, as I don't want to lose the ease of use of the original approach and I don't want to force anyone using the script adding a new global to their projects.
Events
One solution I have found is leveraging events. By putting something like this in the plugin body:
elem.addEventListener('init', function() {
changeColor.init(elem);
}, false);
anybody will be able to just create an event an fire it accordingly. An example in this case:
var event = new CustomEvent('init', {});
span.dispatchEvent(event);
This would re-init the plugin whenever needed. A working fiddle is to be found here:
http://jsfiddle.net/gleezer/tgztjdzL/1/
The question (finally)
My question is: is there a cleaner/better way of handling this?
How can i let people using this plugin without the need of a global or having to initialise the script themselves the first time? Is event the best way or am I missing some more obvious/better solutions?
You can override Element.setAttribute to trigger your plugin:
var oldSetAttribute = Element.prototype.setAttribute;
Element.prototype.setAttribute = function(name, value) {
oldSetAttribute.call(this, name, value);
if (name === 'data-color') {
changeColor.init(this);
}
}
Pros:
User does not have to explicitly re-initialize the plugin. It will happen automatically when required.
Cons:
This will, of course, only work if the user changes data-color attributes using setAttribute, and not if they create new DOM elements using innerHTML or via some other approach.
Modifying host object prototypes is considered bad practice by many, and for good reasons. Use at your own risk.
There is probably a very simple explanation for this (and likely a much cleaner way to do it), but I'm new and can't quite figure it out. Any assistance would help the learning process...
I have one script that displays one div or another based (show instead of hide) on what a user selects from a dropdown list. Like so:
var PickDiv = (function(){
var _obj = {};
var hideShow = function(elem){
if($(elem).val() === '1'){
$("#slider_action").show();
$("#slider_dollar").hide();
}else if($(elem).val() === '2'){
$("#slider_dollar").show();
$("#slider_action").hide();
}else{
$("#slider_dollar, #slider_action").hide();
}
};
_obj.checkValue = function(){
hideShow($('#build_opt'))
};
var events = function(){
$('#build_opt').change(function(){
hideShow(this);
});
};
$(document).ready(function(){
events ();
checkValue ();
});
return _obj;
}());
This works great and displays the right div based on what is selected from the dropdown. I thought I could reuse this same idea later in my code to have the same effect. Once the div is displayed (after making a selection related to script above), I need to provide another dropdown with additional options. The user will select one of these and then a div will display. So, I figured, I could use something like this:
var RunRate = (function(){
var _obj2 = {};
var hideShow_2 = function(elem_2){
if($(elem_2).val() === '6'){
$("#db_sign").show();
$("#app_down", "#in_redem", "#site_vis", "#cont_ent" ).hide();
}else if($(elem_2).val() === '7'){
$("#app_down").show();
else{
$("#app_down", "#in_redem", "#site_vis", "#db_sign", "#cont_ent" ).hide();
}
};
_obj2.checkValue_2 = function(){
hideShow_2($('#action_type_2'))
};
var events_2 = function(){
$('#action_type_2').change(function(){
hideShow_2(this);
});
};
$(document).ready(function(){
events_2 ();
checkValue_2 ();
});
return _obj2;
}());
Of course, this doesn't work. I tried a number of different things with no luck. Note that if I exclude the first script from my code, the second script works fine, so I know it works. I'm guessing it has something to do with the two scripts sharing a variable or something about jquery that I'm clearly missing.
Any help would be appreciated. Overall, looking to be able to do a number of these types of dependent dropdowns without interfering with one another.
Thanks for your help!
UPDATE:
Note that if in the second script, I replace:
$(document).ready(function(){
with
$( window ).load(function() {
then the problem is solved. So, clearly the problem is related to the document.ready interfering with each other, but I don't know how to fix this without this "hack" above especially if I want to use more of these dependent dropdowns. Is there a way to pass a different variable and call that instead of document?
UPDATE 2
Figured out the problem...my original code was throwing an error due to an undefined reference (checkValue). That error was causing the document ready to not work in the second function. Referenced a more detailed explanation in my answer below.
Figured out the problem thanks to this answer to a related question pointed out by #skmasq. #JustinWood was onto something with his comment on my original question. Turns out that my scripts were throwing an error ("Uncaught ReferenceError: checkValue is not defined"), which was not allowing the document ready function to work properly.
Here's the critical part of the answer :
It is important to note that each jQuery() call must actually return. If an exception is thrown in one, subsequent (unrelated) calls will never be executed.
This applies regardless of syntax. You can use jQuery(), jQuery(function() {}), $(document).ready(), whatever you like, the behavior is the same. If an early one fails, subsequent blocks will never be run.
Could be that $(document).ready() overwrites the initial job? It doesn't matter if you use different functions/variable names ...$(document).ready is the same ...
I'm slowly getting a better understanding of JavaScript but I'm stuck on how best to tackle this particular organization/execution scenario.
I come from a C# background and am used to working with namespaces so I've been reading up on how to achieve this with JavaScript. I've taken what was already starting to become a large JavaScript file and split it out into more logical parts.
I've decided on a single file per page for page specific JavaScript with anything common to two or more pages, like reusable utility functions, in another namespace and file.
This makes sense to me at the moment and seems to be a popular choice, at least during the development process. I'm going to use a bundling tool to combine these disparate files for deployment to production anyway so anything that makes development more logical and easier to find code the better.
As a result of my inexperience in dealing with lots of custom JavaScript I had a function defined in the common JavaScript file like this:
common.js
$(document).ready(function () {
var historyUrl = '/history/GetHistory/';
$.getJSON(historyUrl, null, function (data) {
$.each(data, function (index, d) {
$('#history-list').append('<li>' + d.Text + '</li>');
});
});
});
This is obviously far from ideal as it is specific to a single page in the application but was being executed on every page request which is utterly pointless and insanely inefficient if not outright stupid. So that led me to start reading up on namespaces first.
After a bit of a read I have now moved this to a page specific file and re-written it like this:
Moved from common.js to historyPage.js
(function(historyPage, $, undefined) {
historyPage.GetHistory = function () {
var historyUrl = '/history/GetHistory/';
$.getJSON(historyUrl, null, function (data) {
$.each(data, function (index, d) {
$('#history-list').append('<li>' + d.Text + '</li>');
});
});
};
}( window.historyPage = window.historyPage || {}, jQuery ));
I found this pattern on the jQuery Enterprise page. I'm not going to pretend to fully understand it yet but it seems to be a very popular and the most flexible way of organizing and executing JavaScript with various different scopes whist keeping things out of the global scope.
However what I'm now struggling with is how to properly make use of this pattern from an execution point of view. I'm also trying to keep any JavaScript out of my HTML Razor views and work in an unobtrusive way.
So how would I now call the historyPage.GetHistory function only when it should actually execute ie: only when a user navigates to the History page on the web site and the results of the function are required?
From looking at the code, it would seem that the easiest test would be to check if the page you are on contains an element with an id of history-list. Something like this:
var $histList = $('#history-list');
if($histList.length > 0){
// EXECUTE THE CODE
}
Though if it really only ever needs to run on one given page, maybe it's just not a good candidate for a shared javascript file.
Using the code I have detailed above in the question I have gotten it working by doing the following:
In _Layout.cshtml
#if (IsSectionDefined("History"))
{
<script type="text/javascript">
$(document).ready(function () {
#RenderSection("History", required: false)
});
</script>
}
In History.cshtml
#section History
{
historyPage.GetHistory();
}
The code is executing as required only when the user requests the History page on the web site. Although the comment from #Dagg Nabbit above has thrown me a curve ball in that I thought I was on the right track ... Hmm ...