Converting Typed Properties in JavaScript Classes - javascript

Just starting to use JavaScript Classes and have a quick question. In the example below, I want to ensure/convert certain properties to be numeric when I create the class. For instance, if the user enters "$10.50" for the unit price, I want the class to only have 10.50 so the total function works. I'm sure this can be done with a getter/setter but I can't quite figure out how to implement it.
<form name="orderform">
order date: <input name="order_date" value="" type=text>
<br>item_name: <input name="item_name" value="" type=text>
<br>unit_price: <input name="unit_price" value="" type=text>
<br>quantity: <input name="quantity" value="0" type=text>
</form>
class OrderItem {
constructor(
order_date,
item_name,
unit_price,
quantity,
) {
this.order_date = order_date;
this.item_name = item_name;
this.unit_price = unit_price;
this.quantity = quantity;
}
get total() {
return this.unit_price * this.quantity;
}
}
const orderitem1 = new OrderItem();
function GetNumber(val)
{
if (typeof val !== 'undefined')
{
return Number(val.replace(/[^0-9.-]+/g, ""));
}
else
{
return 0;
}
}
function getOrder()
{
$("#orderform").serializeArray().map(function(x){orderitem1[x.name] = x.value;});
var total = orderitem.total; //doesn't work if they enter '$10.50'
//...do stuff here with the orderitem1 class....
}

Instead of creating the OrderItem and then updating its values in getOrder, you could create the OrderItem on the spot.
The snippet below instantiates an OrderItem from the form data on submit, and parses the numeric values in the constructor using your existing GetNumber function.
You could also consider changing the input type from 'text' to 'number' for those inputs, which would (mostly) prevent users from entering $ in the first place.
// get a reference to the form
const form = document.querySelector('form[name=orderform]');
// add an onsubmit handler
form.addEventListener('submit', (e) => {
// stop the form from submitting
e.preventDefault();
// convert the form's inputs into a key-value mapping, e.g. { quantity: "12", unit_price: "$10.50", ... }
const data = new FormData(e.target);
const config = [...data.entries()].reduce((acc, [key, value]) => ({...acc, [key]: value}), {})
// instantiate an OrderItem from the config
const orderItem = new OrderItem(config);
// do whatever you need to do with it
console.log(orderItem.total);
})
class OrderItem {
constructor({
order_date,
item_name,
unit_price,
quantity,
} = {}) {
this.order_date = order_date;
this.item_name = item_name;
this.unit_price = GetNumber(unit_price);
this.quantity = GetNumber(quantity);
}
get total() {
return this.unit_price * this.quantity;
}
}
function GetNumber(val) {
if (typeof val !== 'undefined') {
return Number(val.replace(/[^0-9.-]+/g, ""));
} else {
return 0;
}
}
<form name="orderform">
order date: <input name="order_date" value="" type=text>
<br>item_name: <input name="item_name" value="" type=text>
<br>unit_price: <input name="unit_price" value="$10.50" type=text>
<br>quantity: <input name="quantity" value="1" type=text>
<button>Get Order</button>
</form>
alternative
If for some reason you need to be able to change the OrderItem's values after instantiating it, you could do it in a getter or setter. (But generally speaking immutability is A Good Thing™.)
class OrderItem {
set unit_price(p) {
// needs a different internal name to avoid recursion
this._unitPrice = toNumber(p);
}
get unit_price() {
return this._unitPrice;
}
}
const toNumber = v => Number(`${v}`.replace(/[^0-9.]/g, ''));
const item = new OrderItem();
item.unit_price = '$10.50';
console.log(item.unit_price);

Related

How to amalgamate an array of Nodelists into a single array

I have a function that takes elements from the DOM to update a db on button click. Currently, there is one id for one value...
<input class='total' doc-id='12345678' value='${whateverCurrent.value}'>user updates</field>
<input class='total' doc-id='87654321' value='${whateverCurrent.value}'>user updates</field>
This is the function:
const elements = document.querySelectorAll('.total')
await Promise.all(Array.from(elements).map(async (el) => {
let docId = el.id;
let total = el.value;
await updateDoc(docId, { total });
}))
I now need to break this down such that there are 4 classes of input fields rather than 1 (by quarter). So there will be different elements but with the same id:
<input class='q1' doc-id='12345678' value='${whateverCurrent.value}'>user updates</field>
<input class='q2' doc-id='87654321' value='${whateverCurrent.value}'>user updates</field>
<input class='q2' doc-id='12345678' value='${whateverCurrent.value}'>user updates</field>
I could run the Promise.all function 4 times, once for each class, but that must be wrong, when instead I should somehow....
// (do something here){
await updateDoc(docId, {q1, q2, q3, q4})
}
when I take all the elements and put them into an array and look at them in the console, I get an array of 4 NodeLists.
How do I take these 4 nodeLists and amalgamate them so that every id has its 4 values to pass to the update function?
I'm not really sure if this is what you're looking for. Can you be more specific in what the updateDoc function expects as arguments?
Anyway, I coded something that collects all the quarterly values per doc-id and produces an object of following form:
{
1234 :
{
q1 : 7
q2 : 9
},
...
}
const elements = document.querySelectorAll('input')
const docsPerQ = {};
elements.forEach(e => {
const docId = e.getAttribute('doc-id');
const val = e.value;
const quarter = e.className;
if(!(docId in docsPerQ)) docsPerQ[docId] = {};
docsPerQ[docId][quarter] = val;
});
console.log(docsPerQ);
<input type="text" class="q1" value="7" doc-id="1234">
<input type="text" class="q1" value="2" doc-id="5678">
<input type="text" class="q2" value="3" doc-id="5678">
<input type="text" class="q2" value="9" doc-id="1234">
EDIT
I changed the code a bit so the produced output is in a more manageable form. It's now an array of objects with some extra keys attached:
[
{
docId: 1234,
quarters: {
q1: 7,
q2: 3
}
},
...
]
const elements = document.querySelectorAll('input');
const QsPerDoc = [];
elements.forEach(e => {
const docId = e.getAttribute('doc-id');
const val = e.value;
const quarter = e.className;
const entry = QsPerDoc.find(e => e.docId === docId);
// find returns undefined if nothing's found. undefined is a falsy value
if(!entry) {
let quarters = {};
quarters[quarter] = val;
QsPerDoc.push({
docId : docId,
quarters
});
}
else {
entry.quarters[quarter] = val;
}
});
console.log(QsPerDoc);
<input type="text" class="q1" value="7" doc-id="1234">
<input type="text" class="q1" value="2" doc-id="5678">
<input type="text" class="q2" value="3" doc-id="5678">
<input type="text" class="q2" value="9" doc-id="1234">
Maybe this works better? Hope it does. I wonder, is the updateDoc function something you can change so it can accept arrays?
You could access them like this:
console.log(QsPerDoc[0].docId);
console.log(QsPerDoc[0].quarters.q1);
(Note: I also changed the name of the object/array to QsPerDoc instead of DocsPerQ, which was not aplty named)
Anyway I have to get back to work instead of procrastinating on stackoverflow ;)

Javascript binding Higher-order function with input onchange not working

My goal is to call a function with a string argument, calling function will return a function which will receive the event object from html input object, and I want to use the string argument in the second function.
const person = {
name:'',
age:''
};
const regForm = (field) => {
console.log('field : ', field);
return event => {
person[field]=event.target.value;
document.getElementById("demo").innerHTML = JSON.stringify(person);
}
};
<input onchange="regForm('name')"/>
<input onchange="regForm('age')"/>
<p id="demo"></p>
The issue is your onchange attribute currently has a string-based value and it is effectively evald when the change event is fired. As #PeterSeliger comments, regForm simply returns a function.
regForm could return anything, and so the default change handler makes no assumptions about your answer. You maybe be expecting that the returned function would be called with the event, but instead the default handler simply discards the value.
One solution would be to use JavaScript's onchange property, instead of HTML's onchange attribute -
const person = {
name:'',
age:''
}
const regForm = field => event =>
person[field] = event.target.value
const form =
document.forms.sample
form.name.onchange = regForm("name")
form.age.onchange = regForm("age")
form.onsubmit = event => {
event.preventDefault()
console.log("Submitted", JSON.stringify(person))
}
<form id="sample">
<input name="name" placeholder="enter your name" />
<input name="age" placeholder="enter your age" />
<input type="submit" />
</form>
And since you are familiar with event delegation, ie event.target, you can remove additional duplication. Looks like regForm just kinda disappeared! -
const data =
{}
const form =
document.forms.sample
form.onchange = event =>
data[event.target.name] = event.target.value
form.onsubmit = event => {
event.preventDefault()
console.log("Submitted", JSON.stringify(data))
}
<form id="sample">
<input name="name" placeholder="enter your name"><br>
<input name="age" placeholder="enter your age"><br>
<input name="foo" placeholder="enter your foo"><br>
<input name="bar" placeholder="enter your bar"><br>
<input type="submit" />
</form>
Output
Submitted {"name":"1","age":"2","foo":"3","bar":"4"}
Functions that take other functions as input and/or return other functions as output are called higher-order functions. There are a variety of terminologies and techniques for dealing with them. For related reading, see What do multiple arrow functions mean?
const preventDefault = f => event =>
( event.preventDefault()
, f(event)
)
const logKeypress = event =>
console.log(event.which)
document
.querySelector('input[name=foo]')
.addEventListener('keydown', preventDefault(logKeypress))
<input name="foo" placeholder="type here to see ascii codes" size="50">
In oder to come close to what the OP might have wished to achieve, one should break down the code into the state-change handling task(, maybe a render task) and the listener-initializing task ...
const person = {
name:'',
age:''
};
function renderPersonStateChange(personReference, key, value) {
personReference[key] = value;
document.body.querySelector('#demo').textContent = JSON.stringify(personReference);
}
function handleTextInputStateChange(evt) {
const elm = evt.currentTarget;
const key = elm.name;
renderPersonStateChange(person, key, elm.value);
}
// initialize event listeners
document.body.querySelectorAll('input').forEach(elm =>
elm.addEventListener('change', handleTextInputStateChange, false)
);
<input name='name' placeholder="enter person's name"/>
<input name='age' placeholder="enter person's age"/>
<p>
<code>
<pre id="demo">
</pre>
</code>
</p>
If one wishes tobind the reference that's state should be changed, the above code then slightly alters (only for the change-handler and the event-initialization parts) towards ...
const person = {
name:'',
age:''
};
function renderPersonStateChange(personReference, key, value) {
personReference[key] = value;
document.body.querySelector('#demo').textContent = JSON.stringify(personReference);
}
function handleStateChangeForBoundPerson(evt) {
const personReference = this;
const elm = evt.currentTarget;
const key = elm.name;
renderPersonStateChange(personReference, key, elm.value);
}
// initialize event listeners
document.body.querySelectorAll('input').forEach(elm =>
elm.addEventListener('change', handleStateChangeForBoundPerson.bind(person), false)
);
<input name='name' placeholder="enter person's name"/>
<input name='age' placeholder="enter person's age"/>
<p>
<code>
<pre id="demo">
</pre>
</code>
</p>

Constructor function didn't save querySelector(x).value

I am working on a small APP for calculating Budgets and i am stuck on a problem i don't understand. I used a constructor for getting the user inputs in a few input fields.
I tried to setup this part in a constructor to learn more about prototyping and constructor functions and to challenge myself. I don't get why the constructor GetInput not holding my input.values
<div class="add">
<div class="add__container">
<select class="add__type">
<option value="inc" selected>+</option>
<option value="exp">-</option>
</select>
<input type="text" class="add__description" placeholder="Add description">
<input type="number" class="add__value" placeholder="Value">
<button class="add__btn"><i class="ion-ios-checkmark-outline"></i></button>
</div>
</div>
const addType = document.querySelector('.add__type').value;
const description = document.querySelector('.add__description').value
const addValue = document.querySelector('.add__value').value;
// EVENTLISTENER Constructor:
function EventListner(selector, listner, fnt) {
this.selector = selector;
this.listner = listner;
this.fnt = fnt;
document.querySelector(selector).addEventListener(listner, fnt);
};
function GetInput(operator, description, value) {
this.operator = operator;
this.description = description;
this.value = value;
}
const inputData = new GetInput(addType, description, addValue);
console.log(inputData);
const clickListener = new EventListner('.add__btn', 'click', () => {
if (description.value == '' || addValue.value == '') {
// MAKE SURE DESCRIPTION AND VALUE IS NOT EMPTY
alert('description and value can\'t be empty');
return;
}
const inputData = new GetInput(addType, description, addValue);
console.log(inputData);
});
const enterKeyListener = new EventListner('.add__value', 'keypress', (e) => {
if (e.keyCode == 13) {
if (description.value == '' || addValue.value == '') {
// MAKE SURE DESCRIPTION AND VALUE IS NOT EMPTY
alert('description and value can\'t be empty');
return;
}
// ON ENTER SAVE VALUES IN AN ARRAY
// IF PLUS INTO incomeArr, ON MINUS INTO expenseArr
console.log('enter pressed');
const inputData = new GetInput(addType, description, addValue);
console.log(inputData);
}
});
Output is:
GetInput {operator: "inc", description: "", value: ""}
Only works when i:
document.querySelector('.add__value').value;
directly into the console.
Your initial values are empty for the input fields. And your code is executing right away. That's why you are getting empty value for those fields.You can try adding some predefined values for those input field.
Code with predefined value for the input fields :
const addType = document.querySelector('.add__type').value;
const description = document.querySelector('.add__description').value
const addValue = document.querySelector('.add__value').value;
function GetInput(operator, description, value) {
this.operator = operator;
this.description = description;
this.value = value;
}
const inputData = new GetInput(addType, description, addValue);
console.log(inputData);
<div class="add">
<div class="add__container">
<select class="add__type">
<option value="inc" selected>+</option>
<option value="exp">-</option>
</select>
<input type="text" value="mydesc" class="add__description" placeholder="Add description">
<input type="number" value="2" class="add__value" placeholder="Value">
<button class="add__btn"><i class="ion-ios-checkmark-outline"></i></button>
</div>
</div>
Note : You need to add event listener to listen to any change in your input field values. If user type anything then you can proceed to instantiate your constructor function
You are getting this issue because you need to trigger an event to for calculate, and one more thing you have to use something else other then const, because you cannot reinitialize value in constant variables, Current code is execute on loading page, So the values is initialized to that will not change
const addType = document.querySelector('.add__type').value;
const description = document.querySelector('.add__description').value
const addValue = document.querySelector('.add__value').value;
So please change const with var or let, Or you can set default value to description and value

how to remember inputs value by using local storage?

I have tried to use local storage to remember the input's value after refreshing page. but my code does not work.
here is the HTML code
<input type="text" name="name" onkeyup="saveValue(event)"/>
<input type="text" name="name" onkeyup="saveValue(event)"/>
<input type="text" name="age" onkeyup="saveValue(event)"/>
and here is javascript
<script type="text/javascript">
var nameArr = ["name"];
var inputs = document.getElementsByName('name');
inputs.forEach(function(el){
el.value = getSavedValue(el);
})
function saveValue(e) {
var name = e.target.name;
var val = e.target.value;
localStorage.setItem(name, val);
}
function getSavedValue(v) {
if (!localStorage.getItem(v)) {
return "";
}
return localStorage.getItem(v);
}
</script>
if there is a way to solve this problem please tell me.
and if there is a way to do that with jquery I will be thankful to tell me that.
Here are couple of things. First instead of onkeyup use onblur so value will be saved in storage only when the focus is removed from the element.
Secondly use a common class inputs in this case and give separate name to each element.
Then get all the elements with same class, iterate through it and get value of name property using getAttribute. Use this value to check if there exist a key in localStorage
var nameArr = ["name"];
var inputs = [...document.getElementsByClassName('inputs')];
inputs.forEach(function(el) {
console.log()
el.value = getSavedValue(el.getAttribute('name'));
})
function saveValue(e) {
var name = e.target.name;
var val = e.target.value;
localStorage.setItem(name, val);
}
function getSavedValue(v) {
if (!localStorage.getItem(v)) {
return "";
}
return localStorage.getItem(v);
}
<input type="text" class='inputs' name="firstName" onblur="saveValue(event)" />
<input type="text" class='inputs' name="lastName" onblur="saveValue(event)" />
<input type="text" class='inputs' name="age" onblur="saveValue(event)" />
On your code you are passing the input object as a parameter instead of its name (or value; you choose). As localStorage only stores String key-value pairs, it won't work as you're trying to find a key that is an object.
in the forEach instead of:
el.value = getSavedValue(el);
set:
el.value = getSavedValue(el.name);
or let the "getSavedValue" function accept an object as parameter, but to access localStorage you must pass a string as the key.

Get elements of a given class under a certain div and get each value

I am trying to get the input values off a form and set them as object properties.
Inside the form there are two divs, one for contact1 details and other for contact2.
To solve the problem I created a javascript object "ContactTab()" with its properties, then I created two instances of it "contact1" and "contact2" and also a method "setName()" to set the name.
Finally I wrote the method "populateContactTab("divID")" which aims to populate "contact1" or "contact2" depending on the div specified in the function.
I am not getting any errors but when I output an instance object the name is empty.
should i be doing this differently?
Thank you.
HTML
<form id="jform" action="" method="get">
<div id="primaryContact" style="float:left;">
<label for="jname">First name:</label>
<input type="text" id="jname" class="contactTab jname" name="fname" value=""><br>
</div>
<div id="secondaryContact" style="float:right;">
<label for="jname">First name:</label>
<input type="text" id="jname" class="contactTab jname" name="fname" value=""><br>
</div>
<input type="submit" value="Submit">
</form>
JAVASCRIPT
function ContactTab() // I specify the object
{
this.name = "";
this.lastName = "";
this.address = "";
this.age = "";
this.phone = "";
}
var contact1 = new ContactTab();//Create two instances
var contact2 = new ContactTab();
function populateContactTab(divID){ //should get values of inputs of specific class and set them to one of the instances.
event.preventDefault();
var objectName;
if (divID == "primaryContact")
{
objectName = "contact1";
}
else (divID == "secondaryContact")
{
objectName = "contact2";
}
$(".contactTab").each( function(index, item){
switch(item)
{
case $(this).hasClass('jname'):
objectName.setName($(this).val());
case 'jlastName':
objectName.setLastName();
case 'jage':
objectName.setAge();
case 'jphone':
objectName.setPhone();
case 'jaddress':
objectName.setAddress();
break;
}
});
}
ContactTab.prototype.setName = function(newName)
{
if (typeof newName != 'undefined')
{
this.name = newName;
}
else
{
document.write("Please enter a valid name");
}
};
There are several problems.
First, you should assign the objects, not their names:
if (divID == "primaryContact")
{
objectName = contact1;
}
else if (divID == "secondaryContact")
{
objectName = contact2;
}
You were also missing an if after else.
Second, you need to restrict your .each() loop to just the DIV you were given to process:
$("#"+divID).find(".contactTab").each(...);
Third, you can't use switch the way you are, it should be a series of ifs:
if ($(this).hasClass("jname")) {
objectName.setName($(this).val());
} else if ($(this).hasClass("jlastName")) {
objectName.setLastName($(this).val());
} else if ($(this).hasClass("jage")) {
objectName.setAge($(this).val());
} ...
switch is just for comparing a value against a series of literals, but item in your case is a jQuery element, not a class name.
Finally, as noted in the comments, you need to make the id attributes of all your elements unique, so you can't reuse id="jname" as you did.

Categories

Resources