I am building a factory function to manage Ship objects for a battleship game. So far I have the following:
const Ship = (name, length, orientation = 'horizontal') => {
let sunk = false;
const hits = Array(length).fill(false);
const hit = (position) => {
if (position <= hits.length) hits[position] = true;
};
function sink() {
sunk = true;
}
return {
name,
length,
orientation,
sunk,
hits,
hit,
sink,
};
};
I am testing the sink() method to change the sunk property boolean from false to true. However, whenever I run:
example.sink()
example.sunk
sunk always remains false.
Where am I going wrong?
For some reason the hit() method alters the hits propertyfine. Butsink()is not altering thesunk` property.
Thanks
You can use getters to retrieve the values:
const Ship = (name, length, orientation = 'horizontal') => {
let sunk = false;
const hits = Array(length).fill(false);
const hit = position => {
if (position <= hits.length) hits[position] = true;
};
function sink() {
sunk = true;
}
return {
name,
length,
orientation,
hit,
sink,
// get values using getters
get sunk() { return sunk; },
get hits() { return hits; },
};
};
const someShip = Ship(`ss something`, 100, `vertical`);
someShip.sink();
console.log(someShip.sunk);
You can create a API method to access the local sunk method
const Ship = (name, length, orientation = "horizontal") => {
let sunk = false;
const hits = Array(length).fill(false);
const hit = (position) => {
if (position <= hits.length) hits[position] = true;
};
function sink() {
sunk = true;
}
function getSunk() {
return sunk;
}
return {
name,
length,
orientation,
hits,
hit,
sink,
getSunk,
};
};
const ship = Ship("a", 10);
console.log(ship.getSunk());
ship.sink();
console.log(ship.getSunk());
Related
Here is the link to my repo's github page, so you can properly see what I mean.
I am currently having an issue with my triviaGame function when trying to make it recursive, but it's sort of "backfiring" on me in a sense.
You'll notice after you answer the first question, everything seems fine. It goes to the next question fine. After that though, it seems like the iterations of it double? The next answer it skips 2. After that, 4. And finally the remaining 2 (adding up to 10, due to how I am iterating over them).
How might I be able to correctly iterate over a recursive function, so it correctly calls all 10 times, and then returns when it is done?
Been struggling with this for hours, and just can't seem to get it to work. My javascript code is below, sorry for any headaches that it may give you. I know I make some questionable programming decisions. Ignore some of the commented out stuff, it's not finished code yet. I'm a beginner, and hope that once I learn what's going on here it will stick with me, and I don't make a stupid mistake like this again.
const _URL = "https://opentdb.com/api.php?amount=1&category=27&type=multiple";
const _questionHTML = document.getElementById("question");
const _answerOne = document.getElementById("answer-1");
const _answerTwo = document.getElementById("answer-2");
const _answerThree = document.getElementById("answer-3");
const _answerFour = document.getElementById("answer-4");
const btns = document.querySelectorAll("button[id^=answer-]");
var runCount = 1;
var correct = 0;
// Credits to my friend Jonah for teaching me how to cache data that I get from an API call.
var triviaData = null;
async function getTrivia() {
return fetch("https://opentdb.com/api.php?amount=1&category=27&type=multiple")
.then((res) => res.json())
.then((res) => {
triviaData = res;
return res;
});
}
// anywhere I want the trivia data:
// const trivia = await getTrivia() --- makes the call, or uses the cached data
const shuffleArray = (array) => {
for (let i = array.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
const temp = array[i];
array[i] = array[j];
array[j] = temp;
}
return array;
};
async function triviaGame() {
const trivia = await getTrivia();
async function appendData() {
let totalAnswers = [
...trivia.results[0].incorrect_answers,
trivia.results[0].correct_answer,
];
// Apparently I need 2 different arrays to sort them because array variables are stored by reference? Learn something new everyday I guess.
let totalAnswers2 = [...totalAnswers];
let sorted = shuffleArray(totalAnswers2);
// Ensures the proper symbol shows instead of the HTML entities
const doc = new DOMParser().parseFromString(
trivia.results[0].question,
"text/html"
);
_questionHTML.textContent = doc.documentElement.textContent;
console.log(trivia.results[0].correct_answer, "- Correct Answer");
// Appends info to the DOM
_answerOne.textContent = sorted[0];
_answerTwo.textContent = sorted[1];
_answerThree.textContent = sorted[2];
_answerFour.textContent = sorted[3];
}
async function checkAnswer() {
btns.forEach((btn) => {
btn.addEventListener("click", (event) => {
console.log(runCount);
if (event.target.textContent === trivia.results[0].correct_answer) {
event.target.style.backgroundColor = "#52D452";
// Disables all buttons after one has been clicked.
btns.forEach((btn) => {
btn.disabled = true;
});
setTimeout(() => {
if (runCount === 10) {
return;
}
runCount++;
correct++;
btns.forEach((btn) => {
btn.disabled = false;
});
btn.style.backgroundColor = "";
document.getElementById(
"amount-correct"
).textContent = `${correct}/10`;
triviaGame();
}, 2000);
} else {
event.target.style.backgroundColor = "#FF3D33";
btns.forEach((btn) => {
btn.disabled = true;
});
// document.getElementById("correct-text").textContent =
// trivia.results[0].correct_answer;
// document.getElementById("correct-answer").style.visibility =
// "visible";
setTimeout(() => {
if (runCount === 10) {
return;
}
// document.getElementById("correct-answer").style.visibility =
// "hidden";
btns.forEach((btn) => {
btn.disabled = false;
btn.style.backgroundColor = "";
});
runCount++;
triviaGame();
}, 3500);
}
});
});
}
checkAnswer();
appendData();
}
triviaGame();
Any/All responses are much appreciated and repsected. I could use any help y'all are willing to give me. The past 6 hours have been a living hell for me lol.
It's skipping questions once an answer is clicked because every time a button is clicked, another event listener is added to the button, while the original one is active:
On initial load: triviaGame() runs which makes checkAnswer() run which adds event listeners to each of the buttons.
Event listeners on buttons: 1.
Answer button is clicked, triviaGame() runs which makes checkAnswer() run which adds event listeners to each of the buttons.
Event listeners on buttons: 2.
Answer button is clicked, triviaGame() runs twice (from the 2 listeners attached) which makes checkAnswer() run twice where both invocations adds event listeners to each of the buttons.
Event listeners on buttons: 4.
etc.
To fix this, I moved the content of checkAnswer() outside of any functions so it only ever runs once. However, doing this, it loses reference to the upper scope variable trivia. To resolve this, I used the triviaData variable instead which checkAnswer() would have access to and I change references in appendData() to match this. Now, triviaGame() function only exists to call appendData() function inside it; there is little point in this so I merge the two functions together into one function, instead of two nested inside each other.
const _URL = "https://opentdb.com/api.php?amount=1&category=27&type=multiple";
const _questionHTML = document.getElementById("question");
const _answerOne = document.getElementById("answer-1");
const _answerTwo = document.getElementById("answer-2");
const _answerThree = document.getElementById("answer-3");
const _answerFour = document.getElementById("answer-4");
const btns = document.querySelectorAll("button[id^=answer-]");
var runCount = 1;
var correct = 0;
// Credits to my friend Jonah for teaching me how to cache data that I get from an API call.
var triviaData = null;
async function getTrivia() {
return fetch("https://opentdb.com/api.php?amount=1&category=27&type=multiple")
.then((res) => res.json())
.then((res) => {
triviaData = res;
return res;
});
}
// anywhere I want the trivia data:
// const trivia = await getTrivia() --- makes the call, or uses the cached data
const shuffleArray = (array) => {
for (let i = array.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
const temp = array[i];
array[i] = array[j];
array[j] = temp;
}
return array;
};
async function appendData() {
triviaData = await getTrivia();
let totalAnswers = [
...triviaData.results[0].incorrect_answers,
triviaData.results[0].correct_answer,
];
// Apparently I need 2 different arrays to sort them because array variables are stored by reference? Learn something new everyday I guess.
let totalAnswers2 = [...totalAnswers];
let sorted = shuffleArray(totalAnswers2);
// Ensures the proper symbol shows instead of the HTML entities
const doc = new DOMParser().parseFromString(
triviaData.results[0].question,
"text/html"
);
_questionHTML.textContent = doc.documentElement.textContent;
console.log(triviaData.results[0].correct_answer, "- Correct Answer");
// Appends info to the DOM
_answerOne.textContent = sorted[0];
_answerTwo.textContent = sorted[1];
_answerThree.textContent = sorted[2];
_answerFour.textContent = sorted[3];
}
btns.forEach((btn) => {
btn.addEventListener("click", (event) => {
console.log(runCount);
if (event.target.textContent === triviaData.results[0].correct_answer) {
event.target.style.backgroundColor = "#52D452";
// Disables all buttons after one has been clicked.
btns.forEach((btn) => {
btn.disabled = true;
});
setTimeout(() => {
if (runCount === 10) {
return;
}
runCount++;
correct++;
btns.forEach((btn) => {
btn.disabled = false;
});
btn.style.backgroundColor = "";
document.getElementById(
"amount-correct"
).textContent = `${correct}/10`;
appendData();
}, 2000);
} else {
event.target.style.backgroundColor = "#FF3D33";
btns.forEach((btn) => {
btn.disabled = true;
});
// document.getElementById("correct-text").textContent =
// trivia.results[0].correct_answer;
// document.getElementById("correct-answer").style.visibility =
// "visible";
setTimeout(() => {
if (runCount === 10) {
return;
}
// document.getElementById("correct-answer").style.visibility =
// "hidden";
btns.forEach((btn) => {
btn.disabled = false;
btn.style.backgroundColor = "";
});
runCount++;
appendData();
}, 3500);
}
});
});
appendData();
<div id="amount-correct"></div>
<h1 id="question"></h1>
<button id="answer-1"></button>
<button id="answer-2"></button>
<button id="answer-3"></button>
<button id="answer-4"></button>
I'm trying to validate a form input with multiple hashtags. I absolutely have to split the input and convert it to lowerCase.
ex) #sun #sea #summer #salt #sand
When I'm typing a hashtag that fails the validation, bubble message pops up and tells me it's wrong.
But if I type the next hashtag correctly, previous bubble message clears and the whole validation fails (form can be sent).
I'm assuming it has something to do with .forEach – possibly there are better solutions I'm not yet aware of.
I'm new to JS and would appreciate your answers very much.
// conditions for validation
const hashtagInput = document.querySelector('.text__hashtags');
const commentInput = document.querySelector('.text__description');
const testStartWith = (hashtag) => {
if (!hashtag.startsWith('#')) {
return 'hashtag should start with #';
}
return undefined;
};
const testShortValueLength = (hashtag) => {
if (hashtag.length === 1) {
return 'hashtag should have something after #';
}
return undefined;
};
const testValidity = (hashtag) => {
const regex = /^[A-Za-z0-9]+$/;
const isValid = regex.test(hashtag.split('#')[1]);
if (!isValid) {
return 'hashtag can't have spaces, symbols like #, #, $, etc, or punctuation marks';
}
return undefined;
};
const testLongValueLength = (hashtag) => {
if (hashtag.length > 20) {
return 'maximum hashtag length is 20 symbols';
}
return undefined;
};
const testUniqueName = (hashtagArray, index) => {
if (hashtagArray[index - 1] === hashtagArray[index]) {
return 'the same hashtag can't be used twice';
}
return undefined;
};
const testHashtagQuantity = (hashtagArray) => {
if (hashtagArray.length > 5) {
return 'only 5 hashtags for each photo';
}
return undefined;
};
const testCommentLength = (commentInput) => {
if (commentInput.value.length >= 140) {
return 'maximum comment length is 140 symbols';
}
return undefined;
};
const highlightErrorBackground = (element) => {
element.style.backgroundColor = '#FFDBDB';
};
const whitenBackground = (element) => {
element.style.backgroundColor = 'white';
};
Here is the validation at work
const testHashtagInput = () => {
const hashtagArray = hashtagInput.value.toLowerCase().split(' ');
hashtagArray.forEach((hashtag, index) => {
let error = testStartWith(hashtag)
|| testShortValueLength(hashtag)
|| testValidity(hashtag)
|| testLongValueLength(hashtag)
|| testUniqueName(hashtagArray, index)
|| testHashtagQuantity(hashtagArray);
if (error) {
highlightErrorBackground(hashtagInput);
hashtagInput.setCustomValidity(error);
} else {
whitenBackground(hashtagInput);
hashtagInput.setCustomValidity('');
}
hashtagInput.reportValidity();
});
if (hashtagInput.value === '') {
whitenBackground(hashtagInput);
hashtagInput.setCustomValidity('');
}
hashtagInput.reportValidity();
};
const testCommentInput = () => {
let error = testCommentLength(commentInput);
if (error) {
highlightErrorBackground(commentInput);
commentInput.setCustomValidity(error);
} else {
whitenBackground(commentInput);
commentInput.setCustomValidity('');
}
commentInput.reportValidity();
};
hashtagInput.addEventListener('input', testHashtagInput);
Yes your forEach reevaluates the entire validity of the input based on the individual items but only the last one actually remains in the end because it's the last one being evaluated.
You could change your evaluation to an array-reducer function; best fit for your intention is the reducer "some" (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/some).
It iterates over the list of items and returns whether any one (or more) of the items fullfills the criterion formulated in your callback function.
I didn't get to test it now, but I guess this should do it:
let error = hashtagArray.some((hashtag, index) => {
return (testStartWith(hashtag)
|| testShortValueLength(hashtag)
|| testValidity(hashtag)
|| testLongValueLength(hashtag)
|| testUniqueName(hashtagArray, index)
|| testHashtagQuantity(hashtagArray));
});
if (error) {
highlightErrorBackground(hashtagInput);
hashtagInput.setCustomValidity(error);
} else {
whitenBackground(hashtagInput);
hashtagInput.setCustomValidity('');
}
hashtagInput.reportValidity();
HTH, cheers
I have data stored in my localStorage and in my component OnInit I check if that data exist filter the table results if not, show all results.
The issue is that this functionality sometimes works and sometimes not! I'm not sure what cause the issue but it's most highly to fail when I hard refresh my page Ctrl+F5
Code commented
searchbar_values: boolean = false;
tableSearch_column: boolean = false;
ngOnInit(): void {
// Get all data from server
this.getList(null);
// Condition 1 (first storage values)
const getstoredata = store.get('searchbar_values')
if (getstoredata) {
this.searchbar_values = true;
this.tableSearch_column = false;
}
// Condition 2 (second storage values)
const getstoredata2 = store.get('tableSearch_column')
if (getstoredata2) {
this.tableSearch_column = true;
this.searchbar_values = false;
}
// if first condition happened do this (sometimes fail)
if (this.searchbar_values) {
const storeData = store.get('searchbar_values')
this.globalSearchService.searchTerm.next(storeData.value);
this.globalSearchService.searchTerm.subscribe((newValue: string) => {
this.searchTerm = newValue;
if (newValue != null) {
this.visible = false;
this.globalSearchService.getSearchBar(newValue).subscribe((data: any) => {
this.listOfData = data.data;
this.limit = data.limit
this.totalPages = data.total
this.page = data.page
this.pages = data.pages
this.isSpinning = false;
store.set('searchbar_values', { value: newValue })
this.searchbar_values = true;
if (this.tableSearch_column) {
store.remove('tableSearch_column');
this.tableSearch_column = false;
}
});
}
});
}
// if second condition happened do this (sometimes fail)
if (this.tableSearch_column) {
const storeData = store.get('tableSearch_column')
this.isSpinning = true;
this.globalSearchService.getTableFilter(storeData.field, storeData.op, 'none', storeData.value).subscribe((data: any) => {
this.listOfData = [];
this.listOfData = data.data;
this.limit = data.limit
this.totalPages = data.total
this.page = data.page
this.pages = data.pages
this.isSpinning = false;
});
}
}
PS: tableSearch_column and searchbar_values will never be stored at the same time in localStorage because I always remove one before storing other one.
So the logic is either one of them is exist or none of them.
Any idea?
Solved
I've moved my storage check into constructor like this:
constructor(
//....
) {
//...
const getstoredata = store.get('searchbar_values')
if (getstoredata) {
this.searchbar_values = true;
this.tableSearch_column = false;
}
const getstoredata2 = store.get('tableSearch_column')
if (getstoredata2) {
this.tableSearch_column = true;
this.searchbar_values = false;
}
}
And then called my conditions in ngOnInit as you see in my question above. now everytime it calls localStorage and return correct values.
I have a function that calculates bounds:
function Canvas() {
this.resize = (e) => {
this.width = e.width;
this.height = e.height;
}
this.responsiveBounds = (f) => {
let cached;
return () => {
if (!cached) {
cached = f(this);
}
return cached;
};
}
}
and I have a function that uses this Canvas object bounds:
function Box(canvas) {
let fBounds = canvas.responsiveBounds(({ width, height }) => {
return {
width,
height
};
});
this.render = () => {
let bounds = fBounds();
console.log(bounds);
};
}
Now when canvas resize function is called, bounds will change, and I need to reflect this change (by clearing the cached variable somehow). I can't make cached global because responsiveBounds is called by multiple users.
I see two different options:
1. Register listeners to the change
function Canvas() {
let listeners = [];
this.resize = (e) => {
this.width = e.width;
this.height = e.height;
listeners.forEach(listener => listener());
};
this.responsiveBounds = (f) => {
let cached;
listeners.push(() => cached = undefined);
return () => {
if (!cached) {
cached = f(this);
}
return cached;
};
}
}
Here each responsiveBounds execution context has its own listener registered, so it can take care of clearing its private cache.
2. Collect all caches in a Weak Map
With a (Weak) Map you can create a store for private cache variables, keyed by the function object for which they are relevant:
function Canvas() {
let cached = new WeakMap;
this.resize = (e) => {
this.width = e.width;
this.height = e.height;
cached.clear();
};
this.responsiveBounds = (f) => {
return () => {
let result = cached.get(f);
if (result === undefined) {
cached.set(f, result = f(this));
}
return result;
};
}
}
The nice thing about Weak Maps is that if the function returned by responsiveBounds gets garbage collected, and also the corresponding f, then the corresponding WeakMap entry can be garbage collected as well.
The advantage of the first solution is that the resize method does not need to know anything about what the listeners are doing (whether they are dealing with a cache or entirely something else).
I have a javascript object with 3 booleans:
var obj = {};
obj.isPrimary = true;
obj.isPromotion = false;
obj.isSocial = false;
Only one of these can be true, there can never be a case where more than 1 is true. How can I achieve this?
Using a getter / setter should do the trick:
var obj = {
set isPrimary(bool){
this.Primary = bool;
if(bool){
this.Promotion = this.Social = false;
}
},
set isSocial(bool){
this.Social = bool;
if(bool){
this.Promotion = this.Primary = false;
}
},
set isPromotion(bool){
this.Promotion = bool;
if(bool){
this.Primary = this.Social = false;
}
},
get isPrimary(){ return this.Primary; },
get isSocial(){ return this.Social; },
get isPromotion(){ return this.Promotion; }
}
obj.isPrimary = true;
obj.isSocial = true;
obj.isPromotion = true;
alert(obj.isPrimary + ' ' + obj.isSocial + ' ' + obj.isPromotion);
// false false true (So only `obj.isPromotion` is true)
You can use Object.defineProperty something like:
Object.defineProperty(obj, 'isPrimary', {
get: function(){
return !this.isPromotion && !this.isSocial; //<-- Your logic goes here.
}
});
Of course the logic behind toggling this option on and off it is up to you.
You can simulate this with a function like this
function createToggleValues() {
var options = {
"primary": 1,
"promotion": 2,
"social": 4
},
value = 0;
return {
"set": function(name) {
value = options[name];
},
"is": function(name) {
return (value & options[name]) === options[name];
}
}
};
var togglable = createToggleValues();
togglable.set("primary");
console.log(togglable.is("primary"));
// true
If you require more options to be added, then you might want to simply extend the options object with new values and the next multiple of 2.
Closures are your friend. They prevent that somebody can fiddle with the internal state.
function createPropObj() {
var props = 0;
return {
get isPrimary() { return props === 1; },
set isPrimary(val) { if (val) { props = 1; }; },
get isPromotion() { return props === 2; },
set isPromotion(val) { if (val) { props = 2; }; },
get isSocial() { return props === 4; },
set isSocial(val) { if (val) { props = 4; }; }
}
}
You can use it like that:
> var test = createPropObj();
undefined
> test.isPrimary = true;
true
> test.isPromotion = true;
true
> test.isPrimary
false
You could create an object responsible for allowing a single flag to be true at a time. That would relieve your obj from implementing that logic. In the example below, SingleTrueFlagGroup holds onto a collection of flags, allowing a single one to be true at a time. Your obj can then double-dispatch on an instance of this object to get the job done.
The advantage of a similar implementation is that adding and removing flags becomes trivial.
var flags = ['isPrimary', 'isPromotion', 'isSocial'],
obj = {
_flags: new SingleTrueFlagGroup(flags)
};
flags.forEach(function (flag) {
Object.defineProperty(obj, flag, {
get: function () { return this._flags.get(flag); },
set: function (value) { this._flags.set(flag, value); }
});
});
obj.isPrimary = true;
obj.isPromotion = true;
console.log(obj.isPrimary); //false
console.log(obj.isPromotion); //true
function SingleTrueFlagGroup(flags) {
this._flags = {};
(flags || []).forEach(this.add.bind(this));
}
SingleTrueFlagGroup.prototype = {
constructor: SingleTrueFlagGroup,
set: function (flagToSet, value) {
var flags = this._flags;
(flags[flagToSet] = !!value) && Object.keys(flags).forEach(function (flag) {
if (flag !== flagToSet) flags[flag] = false;
});
},
get: function (flag) { return this._flags[flag]; },
add: function (flag) { this._flags[flag] = false; }
};
The other answers may work, but they seem over-engineered*.
If the values are exclusive, you should represent them in a single property, not three separate booleans:
obj.type = 'primary' // or 'promotion' or 'social'
Assignment among different values is inherently exclusive, since the variable can only hold one value at a time.
Or if you don't like magic strings, you can use an enumeration of defined values:
const TYPE_PRIMARY = 0
const TYPE_PROMOTION = 1
const TYPE_SOCIAL = 2
...
obj.type = TYPE_PRIMARY