Avoiding duplication of code for jquery function - javascript

I have this script (one of my first) which I have had a bit of help developing:
http://jsfiddle.net/spadez/AYUmk/2/
var addButton =$("#add"),
newResp = $("#resp_input"),
respTextArea = $("#responsibilities"),
respList = $("#resp");
//
function Responsibility(text){
this.text=text;
}
var responsibilities = [];
function render(){
respList.html("");
$.each(responsibilities,function(i,responsibility){
var el = renderResponsibility(responsibilities[i],function(){
responsibilities.splice(i,1);//remove the element
render();//re-render
});
respList.append(el);
});
respTextArea.text(responsibilities.map(function(elem){
return elem.text;//get the text.
}).join("\n"));
}
addButton.click(function(e){
var resp = new Responsibility(newResp.val());
responsibilities.push(resp);
render();
newResp.val("");
});
function renderResponsibility(rep,deleteClick){
var el = $("<li>");
var rem = $("<a>Remove</a>").click(deleteClick);
var cont = $("<span>").text(rep.text+" ");
return el.append(cont).append(rem);
}
Using the top box you can add responsibilities into the text area by typing them into the input box and clicking add. This works perfectly for my first box, but I need this to work for three different boxes and now I'm getting a bit stuck on how to apply this function to all three instances "responsibility, test, test2" without simply duplicating the code three times and changing the variables.
I'm sure this type of thing must come up a lot but I'm not sure if it can be avoid. Hopefully someone with more javascript experience can shed some light on this.

You can e.g. use the scoping of javascript for this:
function Responsibility(text){
/* .... */
}
function setUp(addButton, newResp, respTextArea, respList) {
var responsibilities = [];
function render(){
/* ..... */
}
addButton.click(function(e){
/* ..... */
});
function renderResponsibility(rep,deleteClick){
/* ..... */
}
}
And then for each group you can call:
setUp($("#add"), $("#resp_input"), $("#responsibilities"), $("#resp") );
You need for sure have either different id for each of this fields like #add1, #add2 ...
or you could also group each of this into e.g. a div with a class like .group1 and use class instead of id like .add , .resp_input then you even could reduce the number of parameters you need to pass to the setup to one paramter (only passing the container)

I modified your code to do exactly what you want.
Live Demo http://jsfiddle.net/AYUmk/5/
The trick is to make your responsibilities array a multidimensional array that holds an array for each item (in this case, 3 items).
var responsibilities = [new Array(),new Array(),new Array()];
Then, I updated the add buttons to have a CLASS of add instead of an ID of add. You should never have more than one element with the same ID anyway. Additionally, I added several data items to the buttons. These data items tell the jQuery which array item to use, which textbox to look for, which list to add to, and which text box to add to.
<input type="button" value="Add" class="add" data-destination='responsibilities' data-source='resp_input' data-list='resp' data-index="0">
...
<input type="button" value="Add" class="add" data-destination='test' data-source='tst_input' data-list='tst' data-index="1">
...
<input type="button" value="Add" class="add" data-destination='test2' data-source='tst2_input' data-list='tst2' data-index="2">
Then it was just a matter of changing your click() and render() functions to handle the data and multidimensional array
function render(list, textarea, index){
list.html("");
$.each(responsibilities[index],function(i,responsibility){
var el = renderResponsibility(responsibilities[index][i],function(){
responsibilities[index].splice(i,1);//remove the element
render();//re-render
});
list.append(el);
});
textarea.text(responsibilities[index].map(function(elem){
return elem.text;//get the text.
}).join("\n"));
}
$('.add').click(function(e){
var source = $('#' + $(this).data('source') ).val();
var index = parseInt($(this).data('index'));
var list = $('#' + $(this).data('list') );
var dest = $('#' + $(this).data('destination') );
var resp = new Responsibility(source);
responsibilities[index].push(resp);
render(list, dest, index);
newResp.val("");
});
NOTE: I did not get the removal working, let me know if you require assistance with that as well and I will assist once I reach my office

I would try something like this > http://jsfiddle.net/AYUmk/4/
I would access the items by class instead of ids
$(".class").find("...");
you just need to outscource responsibilities = []; and then it works perfect...
but i wont do the whole worke for you :)

Related

How can i add same multiple html element with js?

I want to build a note taker app with html css and js but when i want add second note there is a problem.
let myNote = "";
let myTitle = "";
let noteInput = document.getElementById("note-input");
let titleInput = document.getElementById("title-input");
let title = document.getElementById("title");
let note = document.getElementById("first-note-p");
let addButton = document.getElementById("addButton");
let removeButton = document.getElementById("remove-button");
let newDiv = document.createElement("div");
let newP = document.createElement("p");
let newH3 = document.createElement("h3");
let newButton = document.createElement("button");
let notePlace = document.getElementById("note-place");
let button = document.getElementsByTagName("button");
let div = document.getElementsByTagName("div");
let paragrapgh = document.getElementsByTagName("p");
let head3 = document.getElementsByTagName("h3");
let tally = 0;
const addNote = () => {
myNote = noteInput.value;
myTitle = titleInput.value;
notePlace.appendChild(newDiv);
div[tally].appendChild(newH3);
div[tally].appendChild(newP);
div[tally].appendChild(newButton);
notePlace = document.getElementById("note-place");
head3[tally].innerText = myTitle;
paragrapgh[tally].innerText = myNote;
button[tally + 1].innerText = "remove";
tally += 1;
};
const removeNote = () => {
title.innerHTML = "";
note.innerHTML = "";
};
addButton.onclick = addNote;
<h1>Take your notes</h1>
<input id="title-input" onfocus="this.value=''" type="text" value="title" />
<input id="note-input" onfocus="this.value=''" type="text" value="note" />
<button id="addButton">add</button>
<div id="note-place"></div>
I use addNote function to add a new note but for second note I encounter to the following error.
Cannot set properties of undefined (setting 'innerText')
at HTMLButtonElement.addNote (notetaker.js:37:26)
Sorry for my bad English.
The main problem with your attempt is that you're selecting the elements before actually creating and appending them to the DOM and that will lead to problems because those elements that were initially selected are no longer there when a new note is added.
The fix is fairly easy, select the elements at the time you create a new note. Actually, I won't just stop here and I will happily invite you to follow along with my answer as we approach your task (of making notes and showing them in the screen) in a better approach that, i think, will be more helpful than just giving a fix.
So, here's what we're going to do, we're firstly go by tackling the task and see what are the main sub-tasks to do in order to have a working demo (with add and remove notes features):
To have a better performance, we'll select and cache the elements that we will use extensively in our task. Mainly, the element div#note-place should be cached because we're going to use many times when we add and remove notes.
The inputs, for the note title and text, the button that adds a note, those elements should be cached as well.
The main thing we will be doing is creating some elements and appending them to div#note-place so we can assign that sub-task to a separate function (that we will create). This function will create an element, add the wanted attributes (text, class etc...) then it returns that created element.
At this stage, our solution has started to take shape. Now, to create a note we will listen for the click event on the add note button and then we will have a listener that will handle the creation of the new note based on the values found on the inputs and then append that to the DOM. We will use addEventListener to attach a click event listener on the add note button (modern JS, no more onclicks!).
Now, for the remove note feature. The initial thinking that comes to mind is that we will listen for click events on the remover buttons and then do the work. This can work, but here's a better solution, Event Delegation, which basically allow us to have 1 listener set on div#note-place element that will call the remove note logic only when a remove button was clicked (see the code below for more info).
So, let's not take more time, the live demo below should allow you to easily understand what's being said:
/** cache the elemnts that we know we will use later on */
const notesContainer = document.getElementById('note-place'),
titleInp = document.getElementById('title-input'),
noteInp = document.getElementById('note-input'),
addNoteBtn = document.getElementById('add-note-btn'),
/** this class will be added to all remove note buttons This will allow us to catch clicks on those buttons using event delegation */
noteRemoverBtnClass = 'note-remover-btn',
/**
* a simple function that create an element, add the requested attribute and return the newly created element.
* tag: the tag name of the element to create (like div, h3 etc...).
* text: the text to show on the element (using textContent attribute).
* attributes: an object that holds "key: value" pairs where the keys are the attributes (like id, type etc...) and the values are the values for each attribute set on that parameter (see usage below).
*/
createElement = (tag, text, attributes) => {
const el = document.createElement(tag);
attributes = attributes || {};
!!text && (el.textContent = text);
for (let attr in attributes)
attributes.hasOwnProperty(attr) && el.setAttribute(attr, attributes[attr]);
return el;
};
/** listen for click events on the add note button */
addNoteBtn.addEventListener('click', () => {
/** create a div that will wrap the new note */
const noteEl = createElement('div');
/**
* create an "h3" for the note title, a "p" for the note text and a "button" that acts as the remove note button
* then loop through them and add them to the note wrapper that we just created
*/
[
createElement('h3', titleInp.value),
createElement('p', noteInp.value),
createElement('button', 'Remove', {
type: 'button',
class: noteRemoverBtnClass
})
].forEach(el => noteEl.appendChild(el));
/** append the entire note element (including the "h3", "p"p and "button" to "div#note-place" */
notesContainer.appendChild(noteEl);
});
/** implement event delegation by listening to click events on "div#note-place" and execute a set of logic (to remove a note) only when the clicked element is actually a remove button (thanks to "noteRemoverBtnClass" that we add to each created remove button) */
notesContainer.addEventListener('click', e => e.target.classList.contains(noteRemoverBtnClass) && e.target.parentNode.remove());
<h1>Take your notes</h1>
<input id="title-input" onfocus="this.value=''" type="text" value="title" />
<input id="note-input" onfocus="this.value=''" type="text" value="note" />
<button id="add-note-btn">add</button>
<div id="note-place"></div>
The above code sample is definitely NOT the only way to get things done, it only aims to be simple while recommending the use of some modern JS technics and logics. There always be more ways to do the task and even some better ways to do it.

Sorting divs based on array order

I have a drag and drop design that, when you rearrange the draggable items, pushes their ids to an array. So i’ll have an array like:
["#fake-block_1","#fake-block_3","#fake-block_2"]
Behind the scenes, I want to rearrange some corresponding divs that share the same numeric value as these blocks, e.g., #fake-block_1 maps on to #real-block_1. I can’t quite seem to grasp how I would get this rearrangement to happen. Heres what I currently have:
$('.js-fake-block’).each(function(i){
$this = $(this);
$array = new Array();
$delimiter = '_';
$array.push($this.attr("id").split($delimiter)[1]);
$array.forEach(function(item,index){
$realBlockId = "#real-block_”+[item];
});
});
So I loop through every “fake block”, split their ID by an underscore (I match fake and real with the same numeric value), add them into an array, and then have the real Ids made up again… but after that I’m lost. No idea how I’d sort the “real blocks” based on this "fake blocks" arrays order.
Here is a simple example of what you're trying to do JSFiddle
function sortDom(selectorArray) {
while (selectorArray.length) {
let $el = $(selectorArray.pop());
$el.parent().prepend($el);
}
}
//Usage//
$('.ordinal').on('click', function() {
sortDom(['#_01', '#_02', '#_03', '#_04', '#_05', '#_06', '#_07', '#_08', '#_09', '#_10']);
});
$('.reversed').on('click', function() {
sortDom(['#_10', '#_09', '#_08', '#_07', '#_06', '#_05', '#_04', '#_03', '#_02', '#_01']);
});
$('.jumbled').on('click', function() {
sortDom(['#_07', '#_01', '#_10', '#_04', '#_02', '#_03', '#_06', '#_05', '#_09', '#_08']);
});
Note this method does not enforce the array elements reference dom elements attached to the same parent and it does not enforce that all child elements of the parent must be referenced in the array. Unreferenced child elements will be pushed to the bottom of the list.
One solution is to use the sort method to describe how to the sort elements and then re-set the html of the parent:
var sortedDivs = $divs.sort(function (a, b) {
// If referring to your array of IDs, you can use indexOf($(a).attr("id"))
return $(a).attr("id") > $(b).attr("id");
});
$("#container").html(sortedDivs);
JsFiddle
I don't know why you need to save numbers in array and loop again to create a new array .. you can just use the next code
$array = []; // use array outside the loop
$('.js-fake-block').each(function(i){
var $this = $(this);
var $delimiter = '_';
var SplitNum = $this.attr("id").split($delimiter);
$array.push("#real-block_" + SplitNum[1]);
});
console.log($array);
Working example
$array = []; // use array outside the loop
$('.js-fake-block').each(function(i){
var $this = $(this);
var $delimiter = '_';
var SplitNum = $this.attr("id").split($delimiter);
$array.push("#real-block_" + SplitNum[1]);
});
console.log($array);
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div class="js-fake-block" id="fake-block_1"></div>
<div class="js-fake-block" id="fake-block_3"></div>
<div class="js-fake-block" id="fake-block_5"></div>
<div class="js-fake-block" id="fake-block_4"></div>
<div class="js-fake-block" id="fake-block_2"></div>

using OOP on .click function in javascript

I am making a webpage that has a baseball strikezone with 25 buttons that will be clickable in 25 locations. I need to know if there is a easier way to do this then what I am doing. Maybe something that will take up far less lines. The button is clicked and then the counter is added by one to another table.
$('#one').click(function(){
counter++;
$('#ones').text(counter);
});
var countertwo = 0;
$('#two').click(function(){
countertwo ++;
$('#twos').text(countertwo);
});
A bit of a guess here, but:
You can store the counter on the button itself.
If you do, and you give the buttons a common class (or some other way to group them), you can have one click handler handle all of them.
You can probably find the other element that you're updating using a structural CSS query rather than id values.
But relying on those ID values:
$(".the-common-class").click(function() {
// Get a jQuery wrapper for this element.
var $this = $(this);
// Get its counter, if it has one, or 0 if it doesn't, and add one to it
var counter = ($this.data("counter") || 0) + 1;
// Store the result
$this.data("counter", counter);
// Show that in the other element, basing the ID of what we look for
// on this element's ID plus "s"
$("#" + this.id + "s").text(counter);
});
That last bit, relating the elements by ID naming convention, is the weakest bit and could almost certainly be made much better with more information about your structure.
You can use something like this:
<button class="button" data-location="ones">One</button>
...
<button class="button" data-location="twenties">Twenty</button>
<div id="ones" class="location">0</div>
...
<div id="twenties" class="location">0</div>
$('.button').on('click', function() {
var locationId = $(this).data('location')
, $location = $('#' + locationId);
$location.text(parseInt($location.text()) + 1);
});
Also see this code on JsFiddle
More clean solution with automatic counter
/* JS */
$(function() {
var $buttons = $('.withCounter'),
counters = [];
function increaseCounter() {
var whichCounter = $buttons.index(this)+1;
counters[whichCounter] = counters[whichCounter] ? counters[whichCounter] += 1 : 1;
$("#counter"+whichCounter).text(counters[whichCounter]);
}
$buttons.click(increaseCounter);
});
<!-- HTML -->
<button class="withCounter">One</button>
<button class="withCounter">Two</button>
<button class="withCounter">Three</button>
<button class="withCounter">Four</button>
<p id="counter1">0</p>
<p id="counter2">0</p>
<p id="counter3">0</p>
<p id="counter4">0</p>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>

How to implement JavaScript abstractly

I'm looking to build a simple ranking system, where divs are generated dynamically, so there could be a couple or a few hundred, and each div can be ranked up or down. I have it working for 1, but I'm not sure how I can do it on a larger scale where each div has a unique id.
HTML
<button class="up">Up</button>
<button class="down">Dowm</button>
<div id="rank">Rank = 0</div>
JavaScript
var rank = 0;
var rankPercent = 0;
var rankTotal = 0;
$('.up').click(function(){
rank++;
rankTotal++;
rankPercent = rank/rankTotal;
$('#rank').text("Rank = "+ rankPercent);
});
$('.down').click(function(){
rankTotal++;
rankPercent = rank/rankTotal;
$('#rank').text("Rank = "+ rankPercent);
});
The easiest way to do this within JQuery is to have class="rank" on that rank div also. Then since you get sender in click button handler (you just need to change signature to $('.up').click(function(eventObject){ ... })) you can search for next element that has .rank class.
That all being said, I STRONGLY recommend you abandon this approach (since it's really error prone) and instead of building components yourself, invest time in learning something like AngularJs.
Take a look at this question if you want to look into AngularJS - How to master AngularJS?
EDIT: Thanks for the downvote... I was busy with making example in JsFiddle:
http://jsfiddle.net/7zrej/
One of many ways would be to leverage data attributes to keep your rank data on your elements themselves. You can generate unique IDs for each div when you generate them, and put those in data attributes for your buttons too.
Your HTML with initial data attributes might look like this:
<button class="up" data-id="0">Up</button>
<button class="down" data-id="0">Down</button>
<div id="rank-0" data-rank="0">0</div>
<button class="up" data-id="1">Up</button>
<button class="down" data-id="1">Down</button>
<div id="rank-1" data-rank="0">0</div>
<button class="up" data-id="2">Up</button>
<button class="down" data-id="2">Down</button>
<div id="rank-2" data-rank="0">0</div>
And your JavaScript like this:
$(".up").click(function() {
var id = $(this).data("id");
var rank = $("#rank-" + id).data("rank");
rank++;
$("#rank-" + id).data("rank", rank).text(rank);
});
$(".down").click(function() {
var id = $(this).data("id");
var rank = $("#rank-" + id).data("rank");
rank--;
$("#rank-" + id).data("rank", rank).text(rank);
});
Live example
Think of it a little differently. Create an upshift() and downshift() function, where the node swaps rank with whatever's above or below it (or do nothing if there's nothing below it).
So,
function upshift (node) {
var nodeAbove = getNodeAbove(node);
if (nodeAbove) swapRanks(node, nodeAbove);
return node;
}
function downshift (node) {
var nodeBelow = getNodeBelow(node);
if (nodeBelow) swapRanks(node, nodeBelow);
return node;
}
As a side thing, perhaps consider doing this with a more Object Oriented approach:
var ranks = [];
function Node (node_id) {
var node = this;
ranks.push(node);
node.rank = ranks.length-1;
}
Node.prototype.upshift = function upshift () {};
Node.prototype.downshift = function upshift () {};
var n1 = new Node(),
n2 = new Node(),
n3 = new Node();
I'm writing up a longer answer but it would be helpful if you could describe in more detail exactly what sort of behavior you're trying to create.
Are you expecting that the div's reorder themselves when ranks change so that they are always in order of highest ranked to lowest ranked?
Question lacks detail.
EDIT: http://codepen.io/shawkdsn/pen/lojwb/
This example shows a dynamic list of elements with adjustable ranks that maintains the order from highest approval rating to lowest.
EDIT2: I should add that this codepen example is purely front-end code (incomplete solution) and any real ranking system would require integration with a back-end layer. Angular is a good option to explore and is relatively easy to pick up.

How to differentiate between DOM elements in jQuery

I have some code that loops over each row of the table and creates a json object. The elements in the rows can be either of the following:
<input type="text" id="myelem"/>
or
<p id="myelem">foo</p>
Notice that the id attribute for the both is same. This is because on the table there is a button Add a new Row when this button is clicked another row is added to the table with a checkbox. When user submits the form the checkbox goes away and the value they entered turns into <p id="myelem">value they entered</p>
Below is the code I'm using for this.
$('.input-row').each(function(index, row) {
var innerObject = {};
var key = $('#myelem', row).val().toUpperCase();
jsonObject[key] = "bar";
});
The above works fine for textboxes becuse I'm using the .val() function. However, how do I get the data from the row if it contains <p id="myelem">foo</p> ??
my pseudo code would be something like this:
$('.input-row').each(function(index, row) {
var innerObject = {};
/*
if #myelem is a text box then use .val()
if #myelem is a <p> tag then use .html()
*/
var key = $('#myelem', row).val().toUpperCase();
jsonObject[key] = "bar";
});
ids should always be globally unique on a page. If you need multiple elements to be referenced, you should use classes. If you set myelem as a class rather than an id you could then reference it like this
$('.input-row .myelem')
You can check which type the element is with
var value = null;
if($('#myid').is('input')) {
value = $('#myid').val();
}
else if($('#myid').is('p')) {
value = $('#myid').html();
}
IDs are unique. You cannot use more than one ID in the same page. If you do so how should you decide which element to use?
You could use jQuery is() eg if $('#myelem').is ('p'){...}
If still want to stick your development way then below might help you:
$('.input-row').each(function(index, row) {
var innerObject = {};
var c = $('#myelem', row);
var isInputField = c.get(0).tagName.toUpperCase()=="INPUT";
var key =isInputField ? c.val().toUpperCase():c.html().toUpperCase();
jsonObject[key] = "bar";
});
This is to just get you started. You are using .each on class input-row but you have not shown the class in your code that you provided. I have used class instead of id in this example. Use it to work ahead.
Fiddle

Categories