VueJS - Click event is not triggered as parent event prevaults - javascript

I am building a search input that fetchs data from my API and lists it in a dropdown list.
Here is the behavior I want my component to have:
If I start typing and my API founds data, it opens the dropdown menu and lists it.
If I click on one of the elements from the list, it is set as 'activeItem' and the dropdown list closes
Else, I can click out of the component (input and dropdown list) and the dropdown list closes
Else, no dropdown list appears and my input works like a regular text input
My issue has to do with Event Bubbling.
My list items (from API) have a #click input that set the clicked element as the 'activeItem'.
My input has both #focusin and #focusout events, that allow me to display or hide the dropdown list.
I can't click the elements in the dropdown list as the #focusout event from the input is being triggered first and closes the list.
import ...
export default {
components: {
...
},
props: {
...
},
data() {
return {
results: [],
activeItem: null,
isFocus: false,
}
},
watch: {
modelValue: _.debounce(function (newSearchText) {
... API Call
}, 350)
},
computed: {
computedLabel() {
return this.required ? this.label + '<span class="text-primary-600 font-bold ml-1">*</span>' : this.label;
},
value: {
get() {
return this.modelValue
},
set(value) {
this.$emit('update:modelValue', value)
}
}
},
methods: {
setActiveItem(item) {
this.activeItem = item;
this.$emit('selectItem', this.activeItem);
},
resetActiveItem() {
this.activeItem = null;
this.isFocus = false;
this.results = [];
this.$emit('selectItem', null);
},
},
emits: [
'selectItem',
'update:modelValue',
],
}
</script>
<template>
<div class="relative">
<label
v-if="label.length"
class="block text-tiny font-bold tracking-wide font-medium text-black/75 mb-1 uppercase"
v-html="computedLabel"
></label>
<div :class="widthCssClass">
<div class="relative" v-if="!activeItem">
<div class="flex items-center text-secondary-800">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-3.5 w-3.5 ml-4 absolute"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
stroke-width="2"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
/>
</svg>
<!-- The input that triggers the API call -->
<input
class="text-black py-2.5 pr-3.5 pl-10 text-black focus:ring-primary-800 focus:border-primary-800 block w-full rounded sm:text-sm border-gray-300"
placeholder="Search for anything..."
type="text"
#input="$emit('update:modelValue', $event.target.value)"
#focusin="isFocus = true"
#focusout="isFocus = false"
>
</div>
<!-- The Dropdown list -->
<Card
class="rounded-t-none shadow-2xl absolute w-full z-10 mt-1 overflow-y-auto max-h-48 px-0 py-0"
v-if="isFocus && results.length"
>
<div class="flow-root">
<ul role="list" class="divide-y divide-gray-200">
<!-- API results are displayed here -->
<li
v-for="(result, index) in results"
:key="index"
#click="setActiveItem(result)" <!-- The event I can't trigger -->
>
<div class="flex items-center space-x-4 cursor-pointer px-4 py-3">
<div class="flex-shrink-0">
<img
class="h-8 w-8 rounded-md ring-2 ring-lighter shadow-lg"
:src="result.image ?? this.$page.props.page.defaultImage.url"
:alt="result.title"
/>
</div>
<div class="min-w-0 flex-1">
<p
class="truncate text-sm font-medium text-black"
:class="{
'text-primary-900 font-bold': result.id === activeItem?.id
}"
>
{{ result.title }}
</p>
<p class="truncate text-sm text-black/75">
{{ result.description }}
</p>
</div>
<div v-if="result.action">
<Link
:href="result.action?.url"
class="inline-flex items-center rounded-full border border-gray-300 bg-white px-2.5 py-0.5 text-sm font-medium leading-5 text-black/75 shadow-sm hover:bg-primary-50"
target="_blank"
>
{{ result.action?.text }}
</Link>
</div>
</div>
</li>
</ul>
</div>
</Card>
</div>
<!-- Display the active element, can be ignored for this example -->
<div v-else>
<article class="bg-primary-50 border-2 border-primary-800 rounded-md">
<div class="flex items-center space-x-4 px-4 py-3">
<div class="flex-shrink-0">
<img
class="h-8 w-8 rounded-md ring-2 ring-lighter shadow-lg"
:src="activeItem.image ?? this.$page.props.page.defaultImage.url"
:alt="activeItem.title"
/>
</div>
<div class="min-w-0 flex-1">
<p class="truncate text-sm font-medium text-black font-bold">
{{ activeItem.title }}
</p>
<p class="truncate text-sm text-black/75 whitespace-pre-wrap">
{{ activeItem.description }}
</p>
</div>
<div class="flex">
<AppButton #click.stop="resetActiveItem();" #focusout.stop>
<svg
class="w-5 h-5 text-primary-800"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M6 18L18 6M6 6l12 12"
></path>
</svg>
</AppButton>
</div>
</div>
</article>
</div>
</div>
</div>
</template>
Here is a look at the input:
With API results (can't click the elements):
When no data is found:
I tried:
handleFocusOut(e) {
console.log(e.relatedTarget, e.target, e.currentTarget)
// No matter where I click:
// e.relatedTarget = null
// e.target = <input id="search" class="...
// e.currentTarget = <input id="search" class="...
}
...
<input
id="search"
class="..."
placeholder="Search for anything..."
type="text"
#input="$emit('update:modelValue', $event.target.value)"
#focusin="isFocus = true"
#focusout="handleFocusOut($event)"
>
The solution:
relatedTarget will be null if the element you click on is not
focusable. by adding the tabindex attribute it should make the element
focusable and allow it to be set as relatedTarget. if you actually
happen to be clicking on some container or overlay element make sure
the element being clicked on has that tabindex="0" added to it so you
can maintain isFocus = true
Thanks to #yoduh for the solution

The root issue looks to be how the dropdown list is being removed from the DOM as soon as the input loses focus because of the v-if on it.
<Card
v-if="isFocus && results.length"
>
This is ok to have, but you'll need to work around it by coming up with a solution that keeps isFocus true whether the focus is on the input or the dropdown. I would suggest your input's #focusout to execute a method that only sets isFocus = false if the focus event's relatedTarget is not any of the dropdown items (can be determined via classname or other attribute). One roadblock to implementing this is that some elements aren't natively focusable, like <li> items, so they won't be set as the relatedTarget, but you can make them focusable by adding the tabindex attribute. Putting it all together should look something like this:
<input
type="text"
#input="$emit('update:modelValue', $event.target.value)"
#focusin="isFocus = true"
#focusout="loseFocus($event)"
/>
...
<li
v-for="(result, index) in results"
:key="index"
class="listResult"
tabindex="0"
#click="setActiveItem(result)"
>
loseFocus(event) {
if (event.relatedTarget?.className !== 'listResult') {
this.isFocus = false;
}
}
setActiveItem(item) {
this.activeItem = item;
this.isFocus = false;
this.$emit('selectItem', this.activeItem);
}

Related

Rails 7 Javascript not working on first page load

I have a Rails 7 app, where users can upload photos to a post. I've used Javascript (client side) to create the drag and drop upload file upload field, but it doesn't work when the page is loaded the first time. If you refresh the page, it works as expected.
I think this is something to do with Turbo, but I don't know how to fix it. I have tried adding <meta name="turbo-visit-control" content="reload"> but with no success.
Any help would be appreciated.
my file upload form and javascript:
<p class="text-2xl font-bold mb-4 text-blue-600 xl:pl-9">Add Some Photos</p>
<p class="text-l font-semibold mb-7 xl:pl-9">You can upload up to ten photos of your item.</p>
<article aria-label="File Upload Modal" class="xl:h-2/5 relative flex flex-col bg-white shadow-xl rounded-md xl:m-7" ondrop="dropHandler(event);" ondragover="dragOverHandler(event);" ondragleave="dragLeaveHandler(event);" ondragenter="dragEnterHandler(event);">
<div id="overlay" class="w-full h-full absolute top-0 left-0 pointer-events-none z-50 flex flex-col items-center justify-center rounded-md">
<i>
<svg class="fill-current w-12 h-12 mb-3 text-blue-700" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<path d="M19.479 10.092c-.212-3.951-3.473-7.092-7.479-7.092-4.005 0-7.267 3.141-7.479 7.092-2.57.463-4.521 2.706-4.521 5.408 0 3.037 2.463 5.5 5.5 5.5h13c3.037 0 5.5-2.463 5.5-5.5 0-2.702-1.951-4.945-4.521-5.408zm-7.479-1.092l4 4h-3v4h-2v-4h-3l4-4z" />
</svg>
</i>
<p class="text-lg text-blue-700">Drop files to upload</p>
</div>
<section class="h-full overflow-auto p-8 w-full h-full flex flex-col">
<header class="border-dashed border-2 border-gray-400 py-12 flex flex-col justify-center items-center">
<p class="mb-3 font-semibold flex flex-wrap justify-center">
<span>Drag and drop your</span> <span>files anywhere or</span>
</p>
<%= f.file_field :photos, multiple: true, class: "hidden", id: "hidden-input", name: "part[photos][]" %>
<button id="button" type="button" class="mt-2 rounded-lg px-3 py-1 bg-blue-600 hover:bg-blue-700 font-bold text-white focus:shadow-outline focus:outline-none">
Upload a file
</button>
</header>
<h1 class="pt-8 pb-3 font-semibold sm:text-lg text-blue-600">
To Upload
</h1>
<ul id="gallery" class="flex flex-1 flex-wrap -m-1">
<li id="empty" class="h-full w-full text-center flex flex-col items-center justify-center items-center">
<img class="mx-auto w-32" src="https://user-images.githubusercontent.com/507615/54591670-ac0a0180-4a65-11e9-846c-e55ffce0fe7b.png" alt="no data" />
<span class="text-small">No files selected.</span>
</li>
</ul>
</section>
</article>
<template id="file-template">
<li class="block p-1 w-1/2 sm:w-1/3 md:w-1/4 lg:w-1/6 xl:w-1/8 h-24">
<article tabindex="0" class="group w-full h-full rounded-md focus:outline-none focus:shadow-outline elative bg-gray-100 cursor-pointer relative shadow-sm">
<img alt="upload preview" class="img-preview hidden w-full h-full sticky object-cover rounded-md bg-fixed" />
<section class="flex flex-col rounded-md text-xs break-words w-full h-full z-20 absolute top-0 py-2 px-3">
<h1 class="flex-1 group-hover:text-blue-800"></h1>
<div class="flex">
<span class="p-1 text-blue-800">
<i>
<svg class="fill-current w-4 h-4 ml-auto pt-1" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<path d="M15 2v5h5v15h-16v-20h11zm1-2h-14v24h20v-18l-6-6z" />
</svg>
</i>
</span>
<p class="p-1 size text-xs text-gray-700"></p>
<button class="delete ml-auto focus:outline-none hover:bg-gray-300 p-1 rounded-md text-gray-800">
<svg class="pointer-events-none fill-current w-4 h-4 ml-auto" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<path class="pointer-events-none" d="M3 6l3 18h12l3-18h-18zm19-4v2h-20v-2h5.711c.9 0 1.631-1.099 1.631-2h5.316c0 .901.73 2 1.631 2h5.711z" />
</svg>
</button>
</div>
</section>
</article>
</li>
</template>
<template id="image-template">
<li class="block p-1 w-1/2 sm:w-1/3 md:w-1/4 lg:w-1/6 xl:w-1/8 h-24">
<article tabindex="0" class="group hasImage w-full h-full rounded-md focus:outline-none focus:shadow-outline bg-gray-100 cursor-pointer relative text-transparent hover:text-white shadow-sm">
<img alt="upload preview" class="img-preview w-full h-full sticky object-cover rounded-md bg-fixed" />
<section class="flex flex-col rounded-md text-xs break-words w-full h-full z-20 absolute top-0 py-2 px-3">
<h1 class="flex-1"></h1>
<div class="flex">
<span class="p-1">
<i>
<svg class="fill-current w-4 h-4 ml-auto pt-" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<path d="M5 8.5c0-.828.672-1.5 1.5-1.5s1.5.672 1.5 1.5c0 .829-.672 1.5-1.5 1.5s-1.5-.671-1.5-1.5zm9 .5l-2.519 4-2.481-1.96-4 5.96h14l-5-8zm8-4v14h-20v-14h20zm2-2h-24v18h24v-18z" />
</svg>
</i>
</span>
<p class="p-1 size text-xs"></p>
<button class="delete ml-auto focus:outline-none hover:bg-gray-300 p-1 rounded-md">
<svg class="pointer-events-none fill-current w-4 h-4 ml-auto" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<path class="pointer-events-none" d="M3 6l3 18h12l3-18h-18zm19-4v2h-20v-2h5.711c.9 0 1.631-1.099 1.631-2h5.316c0 .901.73 2 1.631 2h5.711z" />
</svg>
</button>
</div>
</section>
</article>
</li>
</template>
<script>
const photos = [];
// Get a reference to the file input element
const fileInput = document.getElementById('hidden-input');
// Listen for changes to the file input element
fileInput.addEventListener('change', function() {
// Get the list of selected files
const files = fileInput.files;
// Loop through the selected files
for (let i = 0; i < files.length; i++) {
// Add the file to the photos array
photos.push(files[i]);
}
});
const fileTempl = document.getElementById("file-template"),
imageTempl = document.getElementById("image-template"),
empty = document.getElementById("empty");
// use to store pre selected files
let FILES = {};
// check if file is of type image and prepend the initialied
// template to the target element
function addFile(target, file) {
const isImage = file.type.match("image.*"),
objectURL = URL.createObjectURL(file);
const clone = isImage
? imageTempl.content.cloneNode(true)
: fileTempl.content.cloneNode(true);
clone.querySelector("h1").textContent = file.name;
clone.querySelector("li").id = objectURL;
clone.querySelector(".delete").dataset.target = objectURL;
clone.querySelector(".size").textContent =
file.size > 1024
? file.size > 1048576
? Math.round(file.size / 1048576) + "mb"
: Math.round(file.size / 1024) + "kb"
: file.size + "b";
isImage &&
Object.assign(clone.querySelector("img"), {
src: objectURL,
alt: file.name
});
empty.classList.add("hidden");
target.prepend(clone);
FILES[objectURL] = file;
}
const gallery = document.getElementById("gallery"),
overlay = document.getElementById("overlay");
// click the hidden input of type file if the visible button is clicked
// and capture the selected files
const hidden = document.getElementById("hidden-input");
document.getElementById("button").onclick = () => hidden.click();
hidden.onchange = (e) => {
for (const file of e.target.files) {
addFile(gallery, file);
}
};
// use to check if a file is being dragged
const hasFiles = ({ dataTransfer: { types = [] } }) =>
types.indexOf("Files") > -1;
// use to drag dragenter and dragleave events.
// this is to know if the outermost parent is dragged over
// without issues due to drag events on its children
let counter = 0;
// reset counter and append file to gallery when file is dropped
function dropHandler(ev) {
ev.preventDefault();
for (const file of ev.dataTransfer.files) {
addFile(gallery, file);
overlay.classList.remove("draggedover");
counter = 0;
}
}
// only react to actual files being dragged
function dragEnterHandler(e) {
e.preventDefault();
if (!hasFiles(e)) {
return;
}
++counter && overlay.classList.add("draggedover");
}
function dragLeaveHandler(e) {
1 > --counter && overlay.classList.remove("draggedover");
}
function dragOverHandler(e) {
if (hasFiles(e)) {
e.preventDefault();
}
}
// event delegation to caputre delete events
// fron the waste buckets in the file preview cards
gallery.onclick = ({ target }) => {
if (target.classList.contains("delete")) {
const ou = target.dataset.target;
document.getElementById(ou).remove(ou);
gallery.children.length === 1 && empty.classList.remove("hidden");
delete FILES[ou];
}
};
function addFilesToPhotosArray() {
// Get the gallery element
const gallery = document.getElementById("gallery");
// Get the selected files from the gallery
const selectedFiles = gallery.querySelectorAll(".selected");
// Get the photos array from the input field
const photosArray = document.getElementById("hidden-input").value;
// Add the selected files to the photos array
for (const selectedFile of selectedFiles) {
photosArray.push(selectedFile);
}
// Update the input field with the new photos array
document.getElementById("hidden-input").value = photosArray;
}
// clear entire selection
document.getElementById("cancel").onclick = () => {
while (gallery.children.length > 0) {
gallery.lastChild.remove();
}
FILES = {};
empty.classList.remove("hidden");
gallery.append(empty);
};
</script>
</div>

Vuejs & Tailwind CSS dynamically adding two input fields with one button click

I'm using vue 3 and tailwind css to build an interface and I'm trying to add two different input fields, after clicking the add button, and remove the two fields after clicking the remove button. Something like this Two dynamic input fields with one button click
Currently, I have it working, but it is showing a wrong format. When I click the add button, it shows two input keys in the first row and two input values in the second row.
Wrong Input format.
When I click on the add button, I want it to show one input key and one input value in the first row, and the same in the next row.
This is the form
<div v-show="type == 'dropdown'" class="grid gap-4 md:grid-cols-5 items-center">
<div v-for="(value, index) in keys" :key="index" class="col-span-2">
<input v-model="value.key" type="text" class="block p-2 w-full text-gray-700 bg-gray-50 rounded-sm border border-gray-300" placeholder="Input key" />
</div>
<div v-for="(value, index) in input_values" :key="index" class="col-span-2 row-span-1">
<input v-model="value.input_value" type="text" class="block p-2 w-full text-gray-700 bg-gray-50 rounded-sm border border-gray-300" placeholder="Input value" />
</div>
<div class="col-span-1 row-span-1">
<span><fa icon="square-plus" class="w-6 h-6 text-green-500 cursor-pointer" #click="add" /></span>
<fa v-show="index != 0" icon="square-minus" class="w-6 h-6 px-2 text-red-500 cursor-pointer" #click="remove(index)" />
</div>
</div>
This the script section
export default {
name: "App",
data() {
return {
keys: [{ key: "" }],
input_values: [{ input_value: "" }],
}
},
add() {
this.keys.push({ key: "" })
this.input_values.push({ input_value: "" })
},
remove(index) {
this.keys.splice(index, 1)
this.input_values.splice(index, 1)
},
}

Create click event on newly appended buttons with JQuery

I want to create alerts that can be hidden, but when I hide one, they are all hidden at the same time.
This is the code I have, where I need to create the "click" event to hide only the newly created button but hide all of them at the same time:
const newAlert = $("#mainBodyContainer").append(`
<div class="alert alert-info shadow-lg p-2 mt-2">
<p>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
class="stroke-current flex-shrink-0 w-6 h-6">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
<span>a</span>
</p>
<strong class="text-xl align-center cursor-pointer alert-del">×</strong>
</div>
`);
const deleteAlertButton = newAlert.find(".alert-del")
deleteAlertButton.on("click", () => {
deleteAlertButton.parent().addClass("hidden");
});
Docs jQuery find: "Get the descendants of each element in the current set of matched elements"
Keyword: elementS.
You don't have a unique selector. Every time you create a new button, your event handler returns ALL .alert-del classes it finds inside #mainBodyContainer. Including the ones already in there.
To solve the issue, make sure each button can be uniquely selected:
let alerts = 1;
function createAlert()
{
const newAlert = $("#mainBodyContainer").append(`
<div class="alert alert-info shadow-lg p-2 mt-2">
<p>
<span>Button ${alerts}</span>
</p>
<strong id="alert_${alerts}" class="text-xl align-center cursor-pointer alert-del">×</strong>
</div>
`);
const deleteAlertButton = $("#alert_"+alerts);
deleteAlertButton.on("click", () => {
deleteAlertButton.parent().addClass("hidden");
});
alerts++;
}
createAlert();
createAlert();
createAlert();
.hidden {
display: none;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div id="mainBodyContainer"></div>

alpine.js table edit-in-place functionality

I'm trying to make a table column editable inline using Alpine.js. The idea is to have an "edit in place" functionality so when a row is double-clicked allows for the content to be editable. The issue I'm having is when a cell is clicked it activates all rows.
The ideal behavior is only the clicked row should be editable, all others should remain uneditable.
I have a preview of the issue here, https://codepen.io/ezeagwulae/pen/ZEKeYGQ
<div x-data="data()" class="p-4">
<div class="uppercase font-bold">shopping items</div>
<template x-for="item in items">
<div>
<a #click.prevent #dblclick="toggleEditingState" x-show="!isEditing" x-text="item.item" class="select-none cursor-pointer underline font-lg text-blue-500"></a>
<input type="text" x-model="text" x-show="isEditing" #click.away="toggleEditingState" #keydown.enter="disableEditing" #keydown.window.escape="disableEditing" class="bg-white focus:outline-none focus:shadow-outline border border-gray-300 rounded-lg py-2 px-4 appearance-none leading-normal w-128" x-ref="input">
</div>
</template>
</div>
In your JS file make sure to get the double-clicked input field with e.target.
In your HTML x-model should be set to item.item.
Here's a working example.
HTML
<div x-data="data()" class="p-4">
<div class="uppercase font-bold">shopping items</div>
<template x-for="item in items">
<div>
<a #click.prevent #dblclick="toggleEditingState" x-show="!isEditing" x-text="item.item" class="select-none cursor-pointer underline font-lg text-blue-500"></a>
<input type="text" x-model="item.item" x-show="isEditing" #click.away="toggleEditingState" #keydown.enter="disableEditing" #keydown.window.escape="disableEditing" class="bg-white focus:outline-none focus:shadow-outline border border-gray-300 rounded-lg py-2 px-4 appearance-none leading-normal w-128" x-ref="input">
</div>
</template>
</div>
JS
function data() {
return {
text: "Double click to edit",
isEditing: false,
toggleEditingState(e) {
const el = e.target
this.isEditing = !this.isEditing;
el.focus()
},
disableEditing() {
this.isEditing = false;
},
items: [
{ id: 1, item: "apple" },
{ id: 2, item: "eggs" },
{ id: 3, item: "milk" }
]
};
}
Any suggestions to make only the clicked row editable and not all rows? For instance, if "eggs" the input field should be shown for this row and the other rows should remain as is
For example like this:
<link href="https://unpkg.com/tailwindcss#^2/dist/tailwind.min.css" rel="stylesheet"/>
<script src="https://cdn.jsdelivr.net/gh/alpinejs/alpine#v2.x.x/dist/alpine.js"></script>
<div
x-data="{
items: [
{ id: 1, item: 'apple', edit: false },
{ id: 2, item: 'eggs', edit: false },
{ id: 3, item: 'milk', edit: false },
]
}"
class="p-4"
>
<div class="uppercase font-bold">shopping items</div>
<template x-for="(item, index) in items">
<div>
<a
#click.prevent
#dblclick="
item.edit = true;
$nextTick(() => $refs[item.id].focus());
"
#click.away="item.edit = false"
x-show="!item.edit"
x-text="item.item"
class="
select-none
cursor-pointer
underline
font-lg
text-blue-500
"
></a>
<input
type="text"
x-model="item.item"
x-show="item.edit"
#click.away="item.edit = false"
#keydown.enter="item.edit = false"
#keydown.window.escape="item.edit = false"
class="
bg-white
focus:outline-none focus:shadow-outline
border border-gray-300
rounded-lg
py-2
px-4
appearance-none
leading-normal
w-128
"
:x-ref="item.id"
/>
</div>
</template>
</div>

adding to state with usestate not working?

I'm trying to maintain a track of use inputs with react's use state.
for some reason this is quite difficult.
the function i'm using right now is,
function handleClick(e) {
setAnswers([...answers, answer ]);
// setTheArray([, newElement]);
setAnswer()
setTime(true);
setStage(Stage + 1);
e.preventDefault();
console.log("The link was clicked.");
console.log(answers);
}
the input section looks like this,
<div className=" items-center ">
<label for="answer" className="sr-only font-thin px-2 pb-4 text-lg">
Answer
</label>
<input
onChange={(e) => setAnswer(e.target.value)}
type="text"
value={answer}
name="answer"
id="answer"
className=" items-center shadow-sm focus:ring-indigo-500 focus:border-indigo-500 block w-full sm:text-sm border-gray-300 rounded-md"
placeholder="Enter Here"
/>
</div>
and the button
<button
onClick={handleClick}
className="bg-blue-500 hover:bg-blue-700shadow-xl font-semibold rounded-full fixed right-8 bottom-20 text-xl px-8 py-3 sm:right-16 sm:bottom-16 text-white sm:text-xl sm:py-3 sm:px-7 flex items-center focus:outline-none"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className="-ml-1 mr-2 h-5 w-5"
>
<circle cx="12" cy="12" r="10"></circle>
<polygon points="16.24 7.76 14.12 14.12 7.76 16.24 9.88 9.88 16.24 7.76"></polygon>
</svg>
<span>Submit </span>
</button>
Essentially I want to keep track of the user inputs over time - I could just create a class based component, but i'd rather just use functions if possible.
This is the entire component,
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { requireAuth } from "util/auth.js";
import Sidebar_Users from "components/dashboard/Sidebar/Sidebar_Users";
import MindVaultSection from "components/dashboard/MindVault/MindVaultSection";
import BottomNavigation from "../../components/dashboard/BottomNavigation/BottomNavigation";
/*
This page takes a measurement of someone's working memory,
being how many digits they
are able to remember in a set time period.
To do this there needs to be:
- Digits made randomly,
- Timer,
- Input for user answers based on the progress through the task,
- Record of medication, or lack thereof
Ideally doing 6 tests total - sending data at each point if possible, perhaps just as a total query at end though.
User Flow
- Initial Component
- Component for remembering Text
- Component for entering
- Rest Component
*/
function makeid(length) {
var result = "";
var characters =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
var charactersLength = characters.length;
for (var i = 0; i < length; i++) {
result += characters.charAt(Math.floor(Math.random() * charactersLength));
}
return result;
}
function ButtonBaseline(props) {
const { time, Colour, handleClick, Stage, onSubmit } = props;
if (time == false)
return (
<button
onClick={handleClick}
className="bg-blue-500 hover:bg-blue-700shadow-xl font-semibold rounded-full fixed right-8 bottom-20 text-xl px-8 py-3 sm:right-16 sm:bottom-16 text-white sm:text-xl sm:py-3 sm:px-7 flex items-center focus:outline-none"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className="-ml-1 mr-2 h-5 w-5"
>
<circle cx="12" cy="12" r="10"></circle>
<polygon points="16.24 7.76 14.12 14.12 7.76 16.24 9.88 9.88 16.24 7.76"></polygon>
</svg>
<span>Get Started</span>
</button>
);
else if (time == "input" && Stage < 6)
return (
<button
onClick={handleClick}
className="bg-blue-500 hover:bg-blue-700shadow-xl font-semibold rounded-full fixed right-8 bottom-20 text-xl px-8 py-3 sm:right-16 sm:bottom-16 text-white sm:text-xl sm:py-3 sm:px-7 flex items-center focus:outline-none"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className="-ml-1 mr-2 h-5 w-5"
>
<circle cx="12" cy="12" r="10"></circle>
<polygon points="16.24 7.76 14.12 14.12 7.76 16.24 9.88 9.88 16.24 7.76"></polygon>
</svg>
<span>Next Step </span>
</button>
);
else if (time == "input" && Stage == 6)
return (
<button
onClick={handleClick}
className="bg-blue-500 hover:bg-blue-700shadow-xl font-semibold rounded-full fixed right-8 bottom-20 text-xl px-8 py-3 sm:right-16 sm:bottom-16 text-white sm:text-xl sm:py-3 sm:px-7 flex items-center focus:outline-none"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className="-ml-1 mr-2 h-5 w-5"
>
<circle cx="12" cy="12" r="10"></circle>
<polygon points="16.24 7.76 14.12 14.12 7.76 16.24 9.88 9.88 16.24 7.76"></polygon>
</svg>
<span>Submit </span>
</button>
);
else
return (
<div className="bg-blue-500 hover:bg-blue-700shadow-xl font-semibold rounded-full fixed right-8 bottom-20 text-xl px-8 py-3 sm:right-16 sm:bottom-16 text-white sm:text-xl sm:py-3 sm:px-7 flex items-center focus:outline-none">
Waiting!
</div>
);
}
function Baseline(props) {
const [Stage, setStage] = useState(0); // walkthrough Status
const [time, setTime] = useState(false); //timer off, on, or in input stage.
const [answer, setAnswer] = useState();
const [answers, setAnswers] = useState([]);
// RandomString generates a random string of length n
var numbers = makeid(0 + 2 * Stage);
//qSLeD9D0PlSE
useEffect(() => {
//this sets the time to wait, being 3 seconds
if (time) {
const timeout = setTimeout(() => {
// do anything, this block runs after the timeout has "expired"
// could even set state
setTime("input");
console.log("The timeout was called.");
}, 3000); // timeout expires in 6000 ms
// make sure to clear the timeout on component unmount to avoid memory loss issues
return () => clearTimeout(timeout);
// this callback function runs only on component unmount, not re-renders
}
}, [time]);
function handleOnChange(e) {
setAnswer(e.target.value);
}
function handleClick(e) {
setAnswers([...answers, answer ]);
// setTheArray([, newElement]);
setAnswer()
setTime(true);
setStage(Stage + 1);
e.preventDefault();
console.log("The link was clicked.");
console.log(answers);
}
let testInput;
if (time == true) {
testInput = (
<>
<div>Stage {Stage}</div>
<div className="underline font-light items-center justify-center">
Hold the numbers in your head!
</div>
<div className="uppercase bg-white tracking-widest text-center font-bold text-5xl shadow p-4 items-center">
{numbers}
</div>
</>
);
} else if (time == "input") {
testInput = (
<>
<div>Stage {Stage}</div>
<div className="font-thin px-2 pb-4 text-lg">Enter Numbers</div>
<div className="bg-white rounded-lg shadow p-4 items-center justify-center">
<div className=" items-center ">
<label for="answer" className="sr-only font-thin px-2 pb-4 text-lg">
Answer
</label>
<input
onChange={handleOnChange}
type="text"
value={answer}
name="answer"
id="answer"
className=" items-center shadow-sm focus:ring-indigo-500 focus:border-indigo-500 block w-full sm:text-sm border-gray-300 rounded-md"
placeholder="Enter Here"
/>
</div>
</div>
</>
);
} else if (time == false) {
testInput = (
<>
<p className="text-lg leading-7 text-gray-500 mb-5">
You are about to do a small short term memory test. A few letters will
flash on your computer monitor for 3 seconds. Your job is to write
down as many letters as you can remember
</p>
</>
);
}
return (
<>
<Sidebar_Users dashboard={"Progress Map"}>
<div className="py-8 min-h-full container mx-auto ">
<div className="mt-2 mb-8 text-3xl leading-8 font-extrabold tracking-tight text-gray-900 sm:text-4xl sm:leading-10">
Working Memory Test
</div>
{testInput}
<ButtonBaseline
time={time}
Stage={Stage}
handleClick={handleClick}
// onSubmit={onSubmit}
/>
</div>
</Sidebar_Users>
</>
);
}
export default Baseline;
Cheers!
Here is how you would keep track of all your answers. Since you don't have all your code dealing with stages I can't make this answer more specific
but, if this is not enough letme know where to elaborate.
import React, { useEffect, useState } from "react";
import "./styles.css";
export default function App() {
const [answers, setAnswers] = useState([]);
const [answer, setAnswer] = useState("");
const [isSubmit, setIsSubmit] = useState(false);
useEffect(() => {
if (answers.length === 6) {
//here you would fire your submit to server
setIsSubmit(true);
}
}, [answers]);
function handleClick(e) {
if (!isSubmit) setAnswers([...answers, answer]);
}
function handleOnChange(e) {
setAnswer(e.target.value);
}
return (
<div className="App">
<input value={answer} onChange={handleOnChange} />
<button onClick={(e) => handleClick(e)}>Submit</button>
<ul>
{answers.map((item) => (
<li key={item}>{item}</li>
))}
</ul>
{isSubmit ? "Has Submitted" : "Has not Submitted"}
</div>
);
}
Update: Added some stage logic

Categories

Resources