I currently try to build a form with javascript which has two functionalities:
1) Adding elements dynamically to a list
2) Identify through a button click a certain element (e.g. with the highest value)
See (wanted to add pictures directly to my post, but I am lacking StackOverflow reputation - so here are they as links):
https://i.ibb.co/KxvV5Ph/Bildschirmfoto-2019-11-03-um-19-12-51.png
First functionality works fine (see above, added installations). The second doesnt. My plan was the following:
1) When an element gets added to the list I also push it as an object of the class "installation" to the array installations = []
2) When I click on "Identify Longest Duration" I iterate through a map function over the installations array (and output the highest value as an alert).
Unfortunately, the installations array is empty when I call it from another function.
Get values from form
var instStorage = document.getElementById("instStorage");
var instMasse = document.getElementById("instMasse");
var instPrice = document.getElementById("instPrice");
var instDischarge = document.getElementById("instDischarge");
const installations = [] ; // empty installations array
Adding values to DOM, Calling another function to add values to installations array
const createInstallation = () => {
... (working code to add vars from 1) to list element in DOM)...
addInstallation(); // calling another function to add installation to installations array
}
Class Definition of installation
class installation {
constructor(storage, masse, price, discharge) {
this.storage = storage;
this.masse = masse;
this.price = price;
this.discharge = discharge;
}
... (getter functions here) ...
summary = () => {
return `Installation Specs: Storage ${this.getStorage()},
Mass ${this.getMasse()}, Price ${this.getPrice()} and Discharge
Rate
${this.getDischarge()}`;
}
}
Adding installation to installations array
const addInstallation = () => {
installations.push(new installation(instStorage, instMasse, instPrice, instDischarge));
(...)
}
When I call for test purposes my summary function within the createInstallation() function (after calling addInstallation()) everything works fine; the first element of the installations array gets displayed:
alert(installations[0].summary());
See:
https://i.ibb.co/Khc6R7r/Bildschirmfoto-2019-11-03-um-19-32-41.png
When I call the summary function from the event listener of the "Identify Longest" button the installations array is suddenly empty, even though I added an element a second before (which should have been added to the installations array).
See:
https://i.ibb.co/80bTFWY/Bildschirmfoto-2019-11-03-um-19-36-48.png
I am afraid that's a problem of scope; but I don't see how to fix it.
Help is appreciated :-)
Thanks a lot!
Get values from form
var instStorage = document.getElementById("instStorage");
That is what is not happening. getElementById() gets you an element, by id. Not its value.
Assuming that instStorage and friends are input elements (which is not shown unfortunately), you may want to change the code to actually get their value:
var instStorage = document.getElementById("instStorage").value;
var instMasse = document.getElementById("instMasse").value;
var instPrice = document.getElementById("instPrice").value;
var instDischarge = document.getElementById("instDischarge").value;
Related
Good Afternoon, I'm hoping for some guidance. I am new to JavaScript and struggling with a project. We have been asked to store new entries into a website application, which once added store into the local storage.
This is what I have so far, but I am at a loss where I am going wrong to get this stored.
The first section surrounded by ** works fine (albeit it doesn't store the entires). It is the last bit where I'm trying to add new code to store the message and invoke the event listener which doesn't work.
Any guidance would be greatly appreciated.
**function addTextEntry(itemKey, initialText, isNewEntry) {
// Create a text element to edit the entry
var textElement = document.createElement("textarea");
textElement.rows = 5;
textElement.placeholder = "(new entry)";
// Set the textarea's value to the given text (if any)
textElement.value = initialText;
// Add a section to the page containing the textarea
addSection(itemKey, textElement);
// If this is a new entry (added by the user clicking a button)
// move the focus to the textarea to encourage typing
if (isNewEntry) {
textElement.focus();
}**
// Create an event listener to save the entry when it changes
function saveEntry() {
// A new version of this function is created every time addTextEntry is called,
// so it can access all the variables available in this call to addTextEntry
console.log("saveEntry called with variables in scope:", {
itemKey,
initialText,
isNewEntry,
textElement,
});
document.getElementById(initialText);
item = makeTextItem(initialText);
localStorage.setItem(itemKey);
textElement.addEventListener("initialText", saveEntry);
}
}
setItem method takes 2 parameters:
keyName: The name of the key in the Local Storage
keyValue: The value for a given key
In you code, you are passing only one parameter itemKey. You should also pass the value for that key as a second parameter.
document.getElementById(initialText);
This statement doesn't do anything with the result. You may want to assign it to a value like const textElement = document.getElementById(initialText);
item = makeTextItem(initialText);
You are missing the code for makeTextItem but that's still irrelevant since item isn't being used anywhere in the code below it.
localStorage.setItem(itemKey);
setItem takes TWO params, the key and the thing you want to save storage.setItem(keyName, keyValue);
textElement.addEventListener("initialText", saveEntry);
addEventListener's first argument should be a valid event. addEventListener(type, listener);
For context, I'm building a JS calculator whereby the values displayed dynamically change in a 'summary box' as the user changes the values in the input fields (using a <form> element).
I have set default values in the <input> elements of the form, but the calculation functions in the summary box only get called as a keyup function when the values in the input fields are changed.
I have managed to get this to work perfectly, however, on page load the summary box values do not change as this is based on the keyup function (which hasn't occurred yet), so even though there are default values in the inputs, the summary box has nothing to calculate from (as it's waiting for the keyup functions to execute).
Now, I have found a workaround by repeating the DOM blocks to replace values on page load, but the code isn't very DRY. I am sure there must be a way to do this. I have tried to put the code block in an array and/or object but I am unable to successfully extract and execute this.
// THESE ARRAY ITEMS REPRESENT THE INPUT FIELDS IN THE HTML
const innerElementsArr = [investment, buying, selling, invFee];
// APPLYING THE DOM CHANGES TO EACH INPUT ELEMENT ABOVE
innerElementsArr.forEach(item => {
item.onkeyup = function() {
invDisplay.innerText = `£${investment.value}`;
netProfit.innerText = `£${grossProfitLoss()}`;
invFeeDisplay.innerText = `£${withInvFee()}`;
netProfitLossDisplay.innerText = `£${netProfitDisplay()}`;
};
});
// DISPLAY ALL CALCULATIONS ON PAGE LOAD
invDisplay.innerText = `£${investment.value}`;
netProfit.innerText = `£${grossProfitLoss()}`;
invFeeDisplay.innerText = `£${withInvFee()}`;
netProfitLossDisplay.innerText = `£${netProfitDisplay()}`;
As you can see, I am repeating myself and would like to know if there is a much more cleaner way to do this.
Thank you in advance.
Thank you to Ouroborus for the answer - it was such a simple workaround.
I created a function including the DOM block and then called that function as a keyup event and a window.onload event.
const valuesToChange = () => {
invDisplay.innerText = `£${investment.value}`;
coinDisplay.innerText = `${coinsOwned()} BTC`;
netProfit.innerText = `£${grossProfitLoss()}`;
invFeeDisplay.innerText = `£${withInvFee()}`;
exitFeeDisplay.innerText = `£${withExitFee()}`;
netProfitLossDisplay.innerText = `£${netProfitDisplay()}`;
};
// CHANGING THE VALUES IN THE SUMMARY AREA
const innerElementsArr = [investment, buying, selling, invFee, exitFee];
innerElementsArr.forEach(item => {
item.onkeyup = function() {
valuesToChange();
};
});
// DISPLAY ALL CALCULATIONS ON PAGE LOAD
window.onload = function() {
valuesToChange();
};
I have put together a fun API for game creation. In the code I create a prototype for Mover and then extend it with several specific prototypes (Gold, Monster, and Hero). Each one is based on a img tag with a given ID. I use type-specific information in the constructor and a single template method for each type. Most of the functional code in Mover depends on those type-specific details. I have included one example for simplicity.
I use method calls in a separate script to create and destroy instances of the Mover child types. When I create and destroy one instance at a time everything works as intended. The image updates, the sound plays and it is removed after the correct delay. If I create two or more, however, only the last one works as expected. So if I make gold, moster, hero. Only the hero will remove correctly. The other two will play the audio, but don't appear to update.
I ran into the same problem when I tried to attach a function to the onclick event for more than one instance. Only the last one worked and the others did nothing. Obviously I'm missing something about the way java handles method assignments. Any explanation you can offer would help.
Thanks,
BSD
function Mover()
{
}
Mover.prototype.InitTag = function()
{
this.HTMLtag.src=this.imageURL;
this.HTMLtag.style.position="absolute";
this.HTMLtag.style.width=characterSize;
this.HTMLtag.style.height=characterSize;
this.Position(Math.floor(Math.random()*(MaxW-characterSize)+(characterSize/2)),Math.floor(Math.random()*(MaxH-characterSize)+(characterSize/2)));
}
Mover.prototype.Destroy = function()
{
var disp = this.HTMLtag.display;
this.HTMLtag.src=this.destroyURL
this.HTMLtag.display = disp;
this.destroyAudio.play();
this.RemoveTag();
}
function Monster(id)
{
this.MonsterID = id;
this.HTMLtag = document.getElementById("monster"+id);
this.imageURL = "monster1.jpg";
this.destroyURL = "monster2.jpg";
this.destroyAudio = monsterAudio;
}
Monster.prototype = new Mover();
Monster.prototype.RemoveTag = function()
{
var mID = this.MonsterID;
setTimeout(function() {field.DeleteMonster(mID)}, 1000);
}
function Hero()
{
this.HTMLtag = document.getElementById("hero");
this.imageURL = "hero1.jpg";
this.destroyURL = "hero2.jpg";
this.destroyAudio = heroAudio;
}
Hero.prototype = new Mover();
Hero.prototype.RemoveTag = function()
{
setTimeout(function() {field.DeleteHero()}, 5000);
}
function Gold(id)
{
this.GoldID = id;
this.HTMLtag = document.getElementById("gold"+id);
this.imageURL = "gold1.jpg";
this.destroyURL = "gold2.jpg";
this.destroyAudio = goldAudio;
}
Gold.prototype = new Mover();
Gold.prototype.RemoveTag = function()
{
var mID = this.GoldID;
setTimeout(function() {field.DeleteGold(mID)}, 1000);
}
---------UPDATE UPDATE UPDATE-----------
I have at least partially fixed the problem. I have gotten it to work, but I still don't know why it didn't function as intended. I noticed that while my browser's (Chrome) developer tools could visually identify the most-recently-added Mover when it was being destroyed, it could not do so with the any other movers.
Tag of most recently added Mover can be identified in Chrome developer tools.
This suggested that Mover.HTMLtag was not actually the same as document.getElementById('mover1'). I was able to confirm this by looking at the variables in the GoldField.DeleteMover. At the line indicated mover.src has not changed, but movers[id].HTMLtag.src has been correctly updated. In the most-recently-added case they were both the same.
GoldField.prototype.DeleteMover = function(id)
{
var isHero = false;
if(this.Hero!=null && id==this.Hero.myID)
{
this.Hero = null;
isHero = true;
}
else if(this.Tower!=null && id==this.Tower.myID)
{
this.Tower = null;
}
var mover = document.getElementById("mover"+id);
if(!isHero)
{
this.tag.removeChild(mover);//<<< HERE HERE HERE HERE
delete this.movers[id];
}
}
So, I changed one line in Mover.Destroy. By finding the tag by ID and setting the src. I was able to reliable behavior. It would appear that Mover.HTMLtag is not reliable the same after the second Mover is added. Any explanation?
Mover.prototype.Destroy = function()
{
document.getElementById(this.HTMLtag.id).src=this.destroyURL;
this.HTMLtag.src=this.destroyURL;//old method
this.destroyAudio.play();
this.RemoveTag();
}
On suspicion that this might extend to other updates to this.HTMLtag I set up some basic movement of the Hero. It works great, but if you add one additional Mover of any kind it no longer moves. That narrows down the question considerably. Why would constructing a second Mover cause the prototype members to change?
So I debug your code and I found the cause of your problem. The problem was when you create a new instance of monster you storing a reference to it on the monster var. And when you delete it you don't delete / update the reference to it. So your delete function myField.DeleteMover(id) try to delete a monster already deleted. How to solve this.
// create an array to keep ref to our instances
var monsters= [];
// var monster = null;
function addMonster()
{
// monster = goldField.AddMonster();⏎
// push every monster in array
monsters.push(goldField.AddMonster());
}
function killMonster()
{
// if array.length is true
if (monsters.length) {
// call the destroy function on the last ref
monsters[monsters.length - 1].Destroy();
// remove the last ref from array using pop
monsters.pop();
}
//monster.Destroy();
}
This is working however I think all of this should be done in the objects itself. And you should not care about it here.
Another advice try to use more array methods. Avoid using delete on array index because it mess with index and count instead use splice(index, 1) same for add item in array use push instead of arbitrary index.
Anyway funny game! Good luck to finish it.
Edit, after your answer I go back an test.
To make it work I do this.
// First go inGoldField.prototype.DeleteMover and replace the ugly delete index by
this.movers.splice(id, 1);
// Then in the Mover.prototype.Destroy
// This part is a a little blurred for me.
// the current HTMLtag looks good but when I console.log like this
console.log('before', this.HTMLtag);
this.HTMLtag = document.querySelector("#mover" + this.myID);
console.log('after', this.HTMLtag);
// They are not equal look like the first is outdated
You should convert all your delete and add to splice and push methods.
This is just a quick debug I don't know why the selector is outdated.
So I check the code again and I make it work without refreshing the selector. The problem is caused by the creation of dom element with innerHTML.
First reset
this.HTMLtag.src=this.destroyURL
Then instead of
//Mover.prototype.Destroy
this.tag.innerHTML+="<img id='mover"+this.moverCount+"'>";
I create a img dom el.
var img = document.createElement("img");
img.setAttribute('id', 'mover' + this.moverCount);
this.tag.appendChild(img);
All Monsters are now deleted with the image.
I don't check for the hero but first you should update your innerHTML and reply if there is still a problem. I don't think there is any problem with some prototype.
What would be the best way to get the 3 last messages and append/push all new messages into a array? Would the following be ok? What if I would like to use $firebaseArray ?
var messages = [];
ref.child('messages').limitToLast(3).on('child_added', function(snap){
messages.push(snap.val());
});
var tempMessages = $firebaseArray(ref.child('messages').limitToLast(3));
messages.$loaded(function(data){
messages = data;
})
What you secretly want is a range query.
You want to start at the last three and listen onwards. You can use the .start() function to create a range query. Once you know where to start the range you can start at the last 3 messages and get every one after that.
Here's a JSBin demo of the last three messages, plus any newly added messages.
The problem is though you have to know what the 3rd to last key is first.
To do that we'll do an initial query of the last three, get the 3rd to last key and then run our range query. This is best done with a .once function, because we only care to get the last three items one time.
function LimitMessagesArray(Ref, $q) {
// return a function so we can alter the limit amount
return function LimitMessagesArray(lastLimit) {
var limitQuery = Ref.child('messages').limitToLast(lastLimit);
// Since it's a one time read we'll use a promise
var deferred = $q.defer();
// Reading only once is necessary
limitQuery.once('value', function(snap) {
var newMessages = [];
// iterate through the snapshot and keep the key around
snap.forEach(function(childSnap) {
var newMessage = childSnap.val();
newMessage.$key = childSnap.key();
newMessages.push(newMessage);
});
// resolve the last 'N' messages
deferred.resolve(newMessages);
});
return deferred.promise;
};
}
Now we have the last 'N' messages in an promise-fulfilled array. We want to take the first key of the array and use that to start the range query. To make this easier we'll create a factory a range of messages.
function StartAtMessagesArray(Ref, $firebaseArray) {
return function StartAtMessagesArray(startingKey) {
var rangeQuery = Ref.child('messages').orderByKey().startAt(startingKey);
return $firebaseArray(rangeQuery);
};
}
Once we have these two pieces we can use them together to listen to the last three messages plus all of the newly added ones.
function MyController($scope, limitMessages, startAtMessages, Ref) {
var lastThreeMessages = limitMessages(3);
// When the last three messages load get the first key
lastThreeMessages.then(function(data) {
var startingKey = data[0].$key;
// Create the sync array of messages from the starting key
$scope.messages = startAtMessages(startingKey);
});
}
Inject firebaseArray() to your controller and define the reference you want the array for. For example:
app.controller("Ctrl", ["$scope", "$firebaseArray",
function($scope, $firebaseArray) {
var desiredRef = new Firebase("URL of your app ");
Now create an array for data for firebaseArray(desiredRef) -
$scope.tempMessages = $firebaseArray(desiredRef);
Through the same firebaseArray() service you can use query() for last three messages -
var query = desiredRef.orderByChild("timestamp").limitToLast(3);
//create another array for last(3)
$scope.lastThree = firebaseArray(query);
Official documentation quotes:
This is a PSEUDO READ-ONLY ARRAY suitable for use in directives like
ng-repeat and with Angular filters (which expect an array).
Another note, with this service push(), splice() and other functions will make the array unable to be updated. So use specific APIs as suggested in the documentation. For instance, $add instead of push().
Further reference and example here.
Im curious about what might be a larger question than I think.
I am using the following code to listen for 'keyup' on a group of text input fields. If the user stops typing for a given amount of time, I send the data to a controller using AJAX.
I decided to try my hand at OOP in javascript to accomplish this. This is because I want a new instance of the timer method for each input field. (To be absolutely clear, Im very new to OOP in javascript so this might be dreadful. Let me know.)
Here is the main class with its methods:
function FieldListener(entity){
t = this;
t.typingTimer; // Timer identifier
t.doneTypingInterval = 1000; // Time in ms. e.g.; 5000 = 5secs
t.entity = entity;
entity.bind("keyup", function(){t.setTimer();});
}
FieldListener.prototype.setTimer = function(){
t = this;
// User is still typing, so clear the timer.
clearTimeout(t.typingTimer);
// Get the field name, e.g.; 'username'
t.entityType = t.entity.attr("name");
// If the value is empty, set it to a single space.
if(!(t.val = t.entity.val())){
t.val = ' ';
}
t.noticeSpan = t.entity.siblings("span");
// Display 'waiting' notice to user.
t.noticeSpan.html('...')
t.typingTimer = setTimeout(function(){t.doneTyping();},t.doneTypingInterval);
}
FieldListener.prototype.doneTyping = function(){
// Encode for passing to ajax route.
t = this;
valueType = encodeURIComponent(t.entityType);
value = encodeURIComponent(t.val);
$.ajax({
url: '/check/'+valueType+'/'+value,
type: 'GET',
processData: false
})
.done(function(validationMessage){
t.noticeSpan.html(validationMessage);
})
.fail(function(){
t.noticeSpan.html("Something went wrong. Please try again.");
});
}
So from here I'd like to be able to create an object of the FieldListener class for every input field.
I know I can do it easily if I have an id for each like so:
var fieldListener = new FieldListener($("#someFieldID"));
But I'd like to iterate over every field with a given class name. Something close to this perhaps?:
i = 0;
$(".info-field").each(function(){
i = new FieldListener($(this));
});
But that doesn't work (and doesn't look very nice).
Any thoughts? (Im also curious about critiques/improvements to the class/methods code as well.)
edit: As per #ChrisHerring's question: The issue is that it seems to create the object but only for the last element in the each() method. So the span associated with the last input field with the class '.info-field' displays the validationMessage returned from AJAX regardless of which field I am typing in.
UPDATE:
It seems like something is wrong with the creation of new objects. For example, if, rather than iterating through the each() method, I simply follow one class initiation with another, like so:
var fieldListener1 = new FieldListener($("#someFieldID"));
var fieldListener2 = new FieldListener($("#someOtherFieldID"));
that fieldListener2 overwrites variables being saved when initiating fieldListener1. This means that when I type into the input field with id "#someFieldID", it behaves as if I am typing into the input field with id "#someOtherFieldID". Thoughts?
UPDATE #2 (solved for now):
It seems that I have solved the issue for now. I needed to add 'var' before 't = this;' in the FieldListener class. Any comments/critiques are still welcome of course. ;)
The t variable is global. The function for the "keyup" event is evaluated dynamically which means it picks up the last value of t.
Change
t = this;
to
var t = this;
I think you want an array of FieldListener objects.
var myListeners = [];
i = 0;
$(".info-field").each(function(){
myListeners[i] = new FieldListener($(this));
i++
});
This'll give you a list of FieldListeners, where myListeners[0] is the listener for the first .info-field on the page, myListeners[1] is the listener for the second, etc.
Edit: It would appear you have solved the problem. This answer may still come in handy later on, though, so I won't delete it. =)
I think you should be using jquery's .on() to handle the binding.
$(body).on({
keyup: function () { HandleKeyUpEvent($(this)); },
keydown: function () { HandleKeyDownEvent($(this)); }
}, ".info-field");
I realize this is a departure from your original coding idea (using prototypes) but it will still be OOP, if that's what you intented to do.