Creating nested multistep form - javascript

Nested flow
I need some help design nested multistep form. I have implemented it with a naive solution for tracking the step number, looking for a way to abstract and improve the architecture.
Currently, I have two states the currentStep and the currentSubStep. These are the handlers for back and continue.
const handleNext = () => {
const subSteps = steps?.[currentStep]?.subSteps;
if (currentStep === steps?.length - 1) {
return;
}
if (currentSubStep === subSteps?.length - 1) {
// move to the next step
setCurrentStep((prev) => prev + 1);
setCurrentSubStep(0);
} else {
// move to the next
setCurrentSubStep((prev) => prev + 1);
}
};
const handleBack = () => {
const subSteps = steps?.[currentStep]?.subSteps;
if (currentStep === 0) {
if (currentSubStep !== 0) {
setCurrentSubStep((prev) => prev - 1);
} else if (currentSubStep === 0) {
setCurrentSubStep(0);
} else {
setCurrentStep((prev) => prev - 1);
}
} else {
if (currentSubStep !== 0) {
setCurrentSubStep((prev) => prev - 1);
} else {
setCurrentStep((prev) => prev - 1);
setCurrentSubStep(subSteps?.length - 1 || 0);
}
}
};

Related

Vuejs vuelidate array for object

I have a problem with the Vue vuelidate. Every time the boxes in the picture are pressed, an object is sent to an array. All boxes can be selected.
The condition is that a maximum of 3 boxes must be selected, if more than 3 are selected, I don't want to send the form.
Below is the code that runs when any box is pressed.
valueSelected(value,index) {
// console.log(index)
// console.log(value)
const i = this.mySelectedValue.indexOf(value)
// console.log('const i',i)
if (i === -1) {
this.mySelectedValue.push(value)
} else {
this.mySelectedValue.splice(i, 1)
}
const findIndex = this.user.positions.findIndex(v => {
return v.position === value.position
})
if (findIndex === -1) {
this.user.positions.push(value)
} else {
this.user.positions.splice(findIndex, 1)
}
},
Considering this is the whole function you call. You can just put an if around it so if 3 options are already selected then it will not trigger. I think this is the easy way out.
valueSelected(value,index) {
// console.log(index)
// console.log(value)
const i = this.mySelectedValue.indexOf(value)
// console.log('const i',i)
if (i === -1) {
if(this.mySelectedValue.length < 3){
this.mySelectedValue.push(value)
}
} else {
this.mySelectedValue.splice(i, 1)
}
const findIndex = this.user.positions.findIndex(v => {
return v.position === value.position
})
if (findIndex === -1) {
if(this.user.positions.length < 3){
this.user.positions.push(value)
}
} else {
this.user.positions.splice(findIndex, 1)
}
},

Different way to know the origin of the click, optimizing my tab system

I'm learning JavaScript, and I've done this code, I just want to be sure that I'm on the right way, would you have done it differently?
What bothers me is especially to have passed parameters to the functions "next" and "updateArrows" to know the origin of the clicks
const changeStep = document.querySelectorAll(".step");
const currentPaginate = document.querySelector(".pagination span.active");
const arrows = document.querySelectorAll(".arrow");
for (let arrow of arrows) {
arrow.addEventListener("click", function () {
updateArrows(arrow);
});
}
for (let step of changeStep) {
step.addEventListener("click", function () {
next(step);
});
}
function updateArrows(arrow, currentStep = null, update = true) {
let nextStep;
if (currentStep == null) {
currentStep = document.querySelector(".step.current");
if (arrow.classList.contains("arrow-bottom")) {
nextStep = currentStep.nextElementSibling;
} else {
nextStep = currentStep.previousElementSibling;
}
} else nextStep = document.querySelector(".step.current");
if (!arrow.classList.contains("impossible")) {
if (nextStep.dataset.id != 1 && nextStep.dataset.id != 5) {
arrows.forEach(function (arrow) {
if (arrow.classList.contains("impossible")) {
arrow.classList.remove("impossible");
}
});
} else if (nextStep.dataset.id == 5) {
if (arrow.previousElementSibling.classList.contains("impossible"))
arrow.previousElementSibling.classList.remove("impossible");
arrow.classList.add("impossible");
} else if (nextStep.dataset.id == 1) {
if (arrow.nextElementSibling.classList.contains("impossible"))
arrow.nextElementSibling.classList.remove("impossible");
arrow.classList.add("impossible");
}
if (update == true) next(nextStep, false);
}
}
function next(step, update = true) {
if (!step.classList.contains("current")) {
const currentStep = document.querySelector(".step.current");
const nextStep = step.dataset.id;
currentStep.classList.remove("current");
step.classList.add("current");
currentPaginate.textContent = "0" + nextStep;
let arrow;
if (currentStep.dataset.id < nextStep) {
arrow = document.querySelector(".arrow-bottom");
} else {
arrow = document.querySelector(".arrow-top");
}
if (update == true) updateArrows(arrow, currentStep, false);
}
}
I see what you mean.
Yes you are right. You can do it better...
Instead of passing the parameter arrow you can read from an event object
const changeStep = document.querySelectorAll(".step");
const currentPaginate = document.querySelector(".pagination span.active");
const arrows = document.querySelectorAll(".arrow");
for (let arrow of arrows) arrow.addEventListener("click", updateArrows);
for (let step of changeStep) step.addEventListener("click", next);
function updateArrows(e, currentStep = null, update = true) {
let arrow = null
e.target ? arrow=e.target : arrow=e
let nextStep;
if (currentStep == null) {
currentStep = document.querySelector(".step.current");
if (arrow.classList.contains("arrow-bottom")) {
nextStep = currentStep.nextElementSibling;
} else {
nextStep = currentStep.previousElementSibling;
}
} else nextStep = document.querySelector(".step.current");
if (!arrow.classList.contains("impossible")) {
if (nextStep.dataset.id != 1 && nextStep.dataset.id != 5) {
arrows.forEach(function (arrow) {
if (arrow.classList.contains("impossible")) {
arrow.classList.remove("impossible");
}
});
} else if (nextStep.dataset.id == 5) {
if (arrow.previousElementSibling.classList.contains("impossible"))
arrow.previousElementSibling.classList.remove("impossible");
arrow.classList.add("impossible");
} else if (nextStep.dataset.id == 1) {
if (arrow.nextElementSibling.classList.contains("impossible"))
arrow.nextElementSibling.classList.remove("impossible");
arrow.classList.add("impossible");
}
if (update == true) next(nextStep, false);
}
}
function next(e, update = true) {
let step = null
e.target ? step = e.target : step=e
if (!step.classList.contains("current")) {
const currentStep = document.querySelector(".step.current");
const nextStep = step.dataset.id;
currentStep.classList.remove("current");
step.classList.add("current");
currentPaginate.textContent = "0" + nextStep;
let arrow;
if (currentStep.dataset.id < nextStep) {
arrow = document.querySelector(".arrow-bottom");
} else {
arrow = document.querySelector(".arrow-top");
}
if (update == true) updateArrows(arrow, currentStep, false);
}
}
This should work, if not please contact me beforehand.
While activating the eventListener an event object is passed to function and e.target is an element which was clicked in this case.
What I did was crucial because you sometimes call this function from an eventListener and sometimes from a code. If the element has e.target then it's from an eventListener and if not then it's from code.
Didn't have a chance to test it since I don't have the rest of the code. Let me know if it works.

javascript - parameter one of two possible values

Is there a way, in javascript (no typescript), to specify that the parameter of a method has to be "one of" [value1, value2]?
For example, if I have a function:
const handleCommentAction = (action) => {
if (action === "add") {
setTotalComments(totalComments + 1);
} else if (action === "delete") {
setTotalComments(totalComments - 1);
}
}
if there any way to specify that action has to be one of ["add", "delete"] ?
// Something like this...
const handleCommentAction = (action: ["add", "delete"]) => {
or is impossible?
It can be enforced at runtime only by throwing an error or something:
const handleCommentAction = (action) => {
if (action === "add") {
setTotalComments(totalComments + 1);
} else if (action === "delete") {
setTotalComments(totalComments - 1);
} else {
throw new Error('wrong parameter');
}
}
A better solution would be to use JSDoc to indicate that the argument must be of a particular type:
/**
* #param {'add' | 'delete'} action - The action to perform
*/
const handleCommentAction = (action) => {
if (action === "add") {
setTotalComments(totalComments + 1);
} else if (action === "delete") {
setTotalComments(totalComments - 1);
}
};
But this is only documentation, not a requirement of consumers of handleCommentAction.
A more elaborate solution (definitely worth it in larger projects, but arguably overkill for small scripts) would be to use TypeScript or some other type-aware system:
const handleCommentAction = (action: 'add' | 'delete') => {
if (action === "add") {
setTotalComments(totalComments + 1);
} else if (action === "delete") {
setTotalComments(totalComments - 1);
}
};

More efficient or concise method for this function?

I have a data set of exercises that can be done when working out, in JSON format. I'm implementing a filtering system on this data, which has three different types of filters users can apply: sort filter, (which takes the current set of exercises and orders them by name a-z or vice versa), muscle group (exercises that fall under the same targeted muscle group) and training program (exercises that fall under the same training program).
Users should be able to select any of the filters and it will apply it straight away, but not all 3 types of filters have to be applied. Therefore, I have come up with the following function in JS:
function applyFilters(filters) {
const sort = filters[0];
const muscles = filters[1];
const programs = filters[2];
const exerciseJSONData = "exercises.json";
$.getJSON(exerciseJSONData, {
format: JSON,
}).done(function (result) {
let filteredArr = [];
if (muscles.length === 0 && programs.length === 0) {
filteredArr = result;
} else if (muscles.length !== 0 && programs.length === 0) {
$.each($(result), function (key, val) {
if (muscles.some((item) => val.MainMuscleGroup.indexOf(item) >= 0)) {
filteredArr.push(this);
}
});
} else if (muscles.length === 0 && programs.length !== 0) {
$.each($(result), function (key, val) {
if (programs.some((item) => val.TrainingProgram.indexOf(item) >= 0)) {
filteredArr.push(this);
}
});
} else {
$.each($(result), function (key, val) {
if (
muscles.some((item) => val.MainMuscleGroup.indexOf(item) >= 0) &&
programs.some((item) => val.TrainingProgram.indexOf(item) >= 0)
) {
filteredArr.push(this);
}
});
}
$("#number-of-exercises").text(filteredArr.length + " Exercises Found");
$("#exercises").empty();
createExerciseHTML(filteredArr);
});
}
This function works as intended, however, I think the readability of the if else statement could be hard to understand for others and breaks the rule of not repeating code. I am wondering if there is a more concise and efficient way of coding this function? Thanks in advance.
P.S. Sorry if there was too much info at the beginning of this post, I wanted to give some context to avoid any confusion readers may have.
You can start by taking the $each out and keep the if in the each.
function applyFilters(filters) {
const sort = filters[0];
const muscles = filters[1];
const programs = filters[2];
const exerciseJSONData = "exercises.json";
$.getJSON(exerciseJSONData, {
format: JSON,
}).done(function (result) {
let filteredArr = [];
var hasMuschle = muscles.length > 0 ? 1 : 0;
var hasProgram = programs.length > 0 ? 1 : 0;
var muscleAndProgram = hasMuschle - hasProgram;
$.each($(result), function (key, val) {
if (muscles.length === 0 && programs.length === 0) {
filteredArr = result;
} else if (muscles.length !== 0 && programs.length === 0) {
if (muscles.some((item) => val.MainMuscleGroup.indexOf(item) >= 0)) {
filteredArr.push(this);
}
} else if (muscles.length === 0 && programs.length !== 0) {
if (programs.some((item) => val.TrainingProgram.indexOf(item) >= 0)) {
filteredArr.push(this);
}
} else {
if (
muscles.some((item) => val.MainMuscleGroup.indexOf(item) >= 0) &&
programs.some((item) => val.TrainingProgram.indexOf(item) >= 0)
) {
filteredArr.push(this);
}
}
})
$("#number-of-exercises").text(filteredArr.length + " Exercises Found");
$("#exercises").empty();
createExerciseHTML(filteredArr);
});
}

VueJS for loop creating components, components seem to be linked

i'm doing a for loop on an array, for each value i'm creating a component. I was under the impression that I need to put a key on the for loop so that Vue knows it is unique?
<div v-for="hours in location.hours" class="businessHours" :key="hours.id">
<business-hours :response="hours" :unique-key="uniqueKey" :default-weekdays="response.weekdays">
<div slot-scope="{ hours, weekdays, title, uniqueKey, toggle, inArray, componentKey }">
<div class="panel-heading v-center">
<div class="field">
Within this component I have got some logic that just selects/deselected checkboxes, however these 2 components seem to be linked. When I click checkboxes on one of them, the other one changes too!
This is my component:
<template>
<div>
<slot :hours="response" :weekdays="weekdays" :title="title" :toggle="toggle" :inArray="inArray" :componentKey="componentKey"></slot>
</div>
</template>
<script>
export default {
props: [ 'response', 'uniqueKey', 'defaultWeekdays' ],
data: function() {
return {
weekdays: this.response.weekdays,
componentKey: this.key()
}
},
created() {
if (this.weekdays === undefined) {
this.weekdays = this.defaultWeekdays;
}
},
methods: {
title: function() {
if (this.weekdays === undefined) return;
let selected = Object.keys(this.weekdays).filter((e) => { if (this.weekdays[e].selected) return e });
if (selected.length === 0) return;
let start = this.weekdays[parseInt(selected[0])].value;
let end = this.weekdays[parseInt(selected[selected.length - 1])].value;
if (start === end) {
return start.charAt(0).toUpperCase() + start.substring(1);
}
return start.charAt(0).toUpperCase() + start.substring(1) + ' - ' + end.charAt(0).toUpperCase() + end.substring(1);
},
toggle: function(index) {
let clicked = this.weekdays[index];
let action = clicked.selected ? 'remove' : 'add';
let selected = Object.keys(this.weekdays).filter((i) => { if (this.weekdays[i].selected) return i });
let start = parseInt(selected[0]);
let middle = parseInt(selected[Math.floor(selected.length / 2)]);
let end = parseInt(selected[selected.length - 1]);
if (isNaN(start) && isNaN(middle) && isNaN(end)) {
start = middle = end = index;
}
// Add and remove multiple days
if (index < (start - 1) && action === 'add') {
for (let i = index; i <= (start - 1); i++) {
this.weekdays[i].selected = true;
}
}
if (index > start && index < middle && action === 'remove') {
for (let i = start; i <= index; i++) {
this.weekdays[i].selected = false;
}
}
if (index > (end + 1) && action === 'add') {
for (let i = end + 1; i <= index; i++) {
this.weekdays[i].selected = true;
}
}
if (index < end && index >= middle && action === 'remove') {
for (let i = index; i <= end; i++) {
this.weekdays[i].selected = false;
}
}
// Add and remove single days
if ((index === (end + 1) || index === (start - 1)) || (index === end || index === start) && action === 'add') {
this.weekdays[index].selected = true;
}
if ((index === end || index === start) && action === 'remove') {
this.weekdays[index].selected = false;
}
},
inArray: function(needle) {
let length = this.weekdays.length;
for(let i = 0; i < length; i++) {
if(this.weekdays[i] === needle) return true;
}
return false;
},
key: function() {
return Math.random().toString(36).replace(/[^a-z0-9]+/g, '').substr(0, 10);
}
}
}
</script>
Can someone lend me a hand here as to why this is happening?

Categories

Resources