I'm creating my component library in vue, and I defined my component checkbox, the code is like this:
<template>
<div class="checkboxcont" :class="{'checkboxcont-selected': isSelected}" #click="clickevent">
<span class="j-checkbox">
<input type="checkbox" />
</span>
<slot></slot>
</div>
</template>
<script>
export default {
data() {
return {
isSelected: false
}
},
methods: {
clickevent(event) {
if(this.isSelected) {
this.isSelected = false;
} else {
this.isSelected = true;
}
}
},
}
</script>
Now, I hope that when I click the checkbox to set the data "isSelected" false, I can give the component class "checkboxcont-selected-last", and when I click other checkbox component, the classname "checkboxcont-selected-last" can be removed, how can I listen my click event to finish it? I try to use native JavaScript code to add the classname of the dom, but it seemed to have nothing when I binded the classname of my component with Vue.js:
clickevent(event) {
if(this.isSelected) {
this.isSelected = false;
this.$el.classList.add("checkboxcont-selected-last");
} else {
this.isSelected = true;
}
}
What should I do to solve this problem, please?
Here is my style code using less:
<style lang="less" scoped rel="stylesheet/less">
#import '../../mixin/mixin.less';
.checkboxcont {
display: inline-block;
&:hover {
cursor: pointer;
.j-checkbox {
border-color: #jbluelight;
}
}
}
.j-checkbox {
position: relative;
top: 0;
left: 0;
width: 12px;
height: 12px;
display: inline-block;
border: 1px solid #border;
border-radius: 3px;
line-height: 12px;
vertical-align: -3px;
margin: 0 5px;
z-index: 20;
transition: all .2s linear;
input {
opacity: 0;
position: absolute;
left: 0;
top: 0;
visibility: hidden;
/*display: none;*/
}
}
.checkboxcont-selected {
.j-checkbox {
background: #jbluelight;
border-color: #jbluelight;
&:after {
content: '';
width: 4px;
height: 7px;
border: 2px solid white;
border-top: none;
border-left: none;
position: absolute;
left: 3px;
top: 0;
z-index: 30;
transform: rotate(45deg) scale(1);
}
}
}
</style>
<style lang="less" rel="stylesheet/less">
#import '../../mixin/mixin.less';
.checkboxcont-selected-last .j-checkbox {
border-color: #jbluelight;
}
</style>
My initial thought is that I add the class by using this.$el after I clicked the component, it can be accessed because I dispatched the click event, and I just can't access the other component:
if(this.isSelected) {
this.isSelected = false;
this.$el.classList.add("checkboxcont-selected-last")
} else {
this.isSelected = true;
}
And I remove the class by using native HTML DOM operation when I dispatch the click event because I can not access the other component, so the complete definition of clickevent is that:
clickevent(event) {
let selectedLast = document.querySelector(".checkboxcont-selected-last");
if(selectedLast) {
selectedLast.classList.remove("checkboxcont-selected-last")
}
if(this.isSelected) {
this.isSelected = false;
this.$el.classList.add("checkboxcont-selected-last")
} else {
this.isSelected = true;
}
}
It looks good, but I can not add classname of my component when I use v-bind to bind my component's classname, is it wrong? And Is it unable to use native HTML DOM operation when I bind my component's classname with Vue?
A better way to dynamically add or remove class can be using v-bind:class. There are different ways you can add a dynamic class based on a vue data variable.
I see you are already using it:
<div class="checkboxcont" :class="{'checkboxcont-selected': isSelected}" #click="clickevent">
So here this div will have only one class : checkboxcont if isSelected is false, and two classes : checkboxcont and checkboxcont-selected if isSelected is true.
Edited:
Given that you want to add a class to DOM on another component, I can think of two ways:
Using Web API: You can do following if you know the id of the element you want to add class using Element.className:
var d = document.getElementById("yourElem") d.className += " otherclass"
Vuex way: You can have a centralised state provided by vue or use vuex to manage state, these state variables can be changed across components, and can be used to add/remove class dynamically.
You can have a look at my this answer to understand more about vuex.
Related
My custom react toast component was working well until I tried to implement automatic dismissal of notifications after a set time.
I am trying to make it so that after a set time, the pop-up "toast" notifications will have a CSS fade-out animation play and then be deleted — unless the user is hovering over that notification, in which case it will hold off on dismissing that one until the user moves their mouse off of it.
Sometimes it works properly, other times it stops displaying anything and adds the notifications back one by one, other times it... well, it just behaves in a very strange and unexpected manner.
Here is my code:
Toast.css
.toast-container {
font-size: 24px;
box-sizing: border-box;
position: fixed;
z-index: 10;
}
.toast-popup {
padding: 12px;
display: flex;
align-items: center;
justify-content: space-between;
width: 500px;
border: solid #f2f2f2;
border-radius: 8px;
box-shadow: 0 0 10px #999;
margin-bottom: 1rem;
opacity: 0.9;
}
.toast-popup:hover {
box-shadow: 0 0 12px deepskyblue;
opacity: 1 !important;
animation-play-state: paused;
}
.success {
background-color: #5cb85c;
}
.info {
background-color: #5bc0de;
}
.warning {
background-color: #f0ad4e;
}
.danger {
background-color: #d9534f;
}
.toast-text {
justify-self: flex-start;
width: 100%;
padding: 6px 0 6px 6px;
opacity: 0.9;
}
.toast-title {
font-weight: 700;
font-size: 32px;
text-align: left;
padding-bottom: 0px;
color: #f2f2f2;
}
.toast-message {
padding-top: 0px;
text-align: left;
color: #f2f2f2;
}
.toast-icon {
float: left;
margin: 0 20px 0 10px;
opacity: 0.9;
}
.toast-icon img {
width: 50px;
height: 50px;
fill: #f2f2f2;
opacity: 0.9;
}
.close-button {
float: right;
align-self: flex-start;
font-weight: 600;
color: #f2f2f2;
background: none;
border: none;
opacity: 0.9;
cursor: pointer;
}
.top-right {
top: 2rem;
right: 2rem;
}
.top-right-slide {
top: 2rem;
right: 2rem;
transition: transform .6s ease-in-out;
animation: toast-in-right .7s;
}
.bottom-right {
bottom: 2rem;
right: 2rem;
}
.bottom-right-slide {
bottom: 2rem;
right: 2rem;
transition: transform .6s ease-in-out;
animation: toast-in-right .7s;
}
.top-left {
top: 2rem;
left: 2rem;
}
.top-left-slide {
top: 2rem;
left: 2rem;
transition: transform .6s ease-in;
animation: toast-in-left .7s;
}
.bottom-left {
bottom: 2rem;
left: 2rem;
}
.bottom-left-slide {
bottom: 2rem;
left: 2rem;
transition: transform .6s ease-in;
animation: toast-in-left .7s;
}
.fadeout {
animation: 4s linear 5s 1 normal forwards running toast-fadeout;
}
#keyframes toast-in-right {
from { transform: translateX(100%); }
to { transform: translateX(0); }
}
#keyframes toast-in-left {
from { transform: translateX(-100%); }
to { transform: translateX(0); }
}
#keyframes toast-fadeout {
from { opacity: 0.9; }
to { opacity: 0; }
}
Toast.js - Please excuse the generous peppering of console.logs...
import React, {useEffect, useState} from 'react';
import icon_success from './icons/feathericons/check-circle.svg';
import icon_info from './icons/feathericons/info.svg';
import icon_warning from './icons/feathericons/alert-triangle.svg';
import icon_danger from './icons/feathericons/alert-octagon.svg';
import './Toast.css';
const Toast = (props) => {
const {toastList, position} = props;
const [list, setList] = useState(toastList);
const [prevId, setPrevId] = useState(0);
// This useEffect updates the list of toasts to display
useEffect(() => {
console.log('useEffect()');
console.log('useEffect() toastList:');
console.log(toastList);
setList([...toastList]);
}, [toastList]);
const markForDeletion = (toast) => {
if( toast.isDeleting ) {
return;
}
console.log(`toast ${toast.id} marked for deletion`)
toast.isDeleting = true;
setTimeout(() => {attemptDeletion(toast)}, 5000);
}
const attemptDeletion = (toast) => {
console.log(`attempting to delete toast ${toast.id}. canDelete = ${toast.canDelete}`);
if( toast.canDelete ) {
deleteToast(toast.id);
}
else {
console.log(`cannot delete toast ${toast.id}. `);
}
}
const getIcon = (variant) => {
switch( variant ) {
case 'success':
return icon_success;
break;
case 'info':
return icon_info;
break;
case 'warning':
return icon_warning;
break;
case 'danger':
return icon_danger;
break;
}
}
const generateId = (toast) => {
if( typeof(toast.id) === 'number' ) {
return toast.id;
}
toast.id = prevId + 1;
setPrevId(toast.id);
return toast.id;
}
const deleteToast = (id) => {
console.log(`deleting toast ${id}`);
const deletionIdxList = list.findIndex(e => e.id === id);
const deletionIdxToastList = toastList.findIndex(e => e.id === id);
console.log(`deletionIdxToastList: ${deletionIdxToastList}`);
if(deletionIdxList == null || deletionIdxList === -1) {
console.log(`cannot find list idx of id ${id}`);
console.log('list:');
console.log(list);
return;
}
if(deletionIdxToastList == null || deletionIdxToastList === -1) {
console.log(`cannot find toastList idx of id ${id}`);
console.log('toastList:');
console.log(toastList);
return;
}
console.log('list before deletion:');
console.log(list);
console.log('toastList before deletion:');
console.log(toastList);
console.log('list[deletionIdxList]:');
console.log(list[deletionIdxList]);
list.splice(deletionIdxList, 1);
console.log('toastList[deletionIdxToastList]:');
console.log(toastList[deletionIdxToastList]);
toastList.splice(deletionIdxToastList, 1);
setList([...list]);
console.log(`toast ${id} deleted successfully`);
console.log('list after deletion:');
console.log(list);
console.log('toastList after deletion:');
console.log(toastList);
}
return (
<>
<div className={`toast-container ${position}`} >
{
list.map((toast, i) => (
<div
key={i}
className={`toast-popup ${toast.variant} ${toast.isDeleting ? (position + ' fadeout') : (position + '-slide')}`}
onLoad={() => {
if( !toast.isLoaded ) {
toast.Id = generateId(toast);
toast.canDelete = true;
toast.isDeleting = false;
toast.isLoaded = true;
console.log(`on load ${toast.id}`);
setTimeout(() => markForDeletion(toast), 500);
}
}}
onMouseOver={() => {
toast.canDelete === true ? toast.canDelete = false : null;
toast.isDeleting === true ? toast.isDeleting = false : null;
console.log(`mouse over ${toast.id}`);
}}
onMouseLeave={() => {
toast.canDelete === false ? toast.canDelete = true : null;
markForDeletion(toast);
console.log(`mouse leave ${toast.id}`);
}}
>
<div className={'toast-icon'}>
<img src={getIcon(toast.variant)} />
</div>
<div className={'toast-text'}>
<div className={'toast-title'}>
{toast.variant.charAt(0).toUpperCase() + toast.variant.slice(1)}
</div>
<div className={'toast-message'}>{toast.message}</div>
</div>
<button
className={'close-button'}
onClick={() => {
toast.canDelete = true;
deleteToast(toast.id)
}
}>
X
</button>
</div>
))
}
</div>
</>
)
}
Toast.defaultProps = {
position: 'bottom-right'
}
export default Toast;
Snippet of Home.js where I am testing this new Toast component - A class component as I'm working on updating a pre-existing application to remove dependency on the react-toastify library
// Leaving out constructor and other irrelevant code...
toastSuccess() {
const newToast = {
variant: 'success',
message: 'This is a test of the success variant toast pop-up.'
}
this.setState({
toastList: [...this.state.toastList, newToast]
});
}
toastInfo() {
const newToast = {
variant: 'info',
message: 'This is a test of the info variant toast pop-up.'
}
this.setState({
toastList: [...this.state.toastList, newToast]
});
}
toastWarning() {
const newToast = {
variant: 'warning',
message: 'This is a test of the warning variant toast pop-up.'
}
this.setState({
toastList: [...this.state.toastList, newToast]
});
}
toastDanger() {
const newToast = {
variant: 'danger',
message: 'This is a test of the danger variant toast pop-up.'
}
this.setState({
toastList: [...this.state.toastList, newToast]
});
}
render() {
return (
<div className="Home" style={{height:'100%'}}>
<Toast
toastList={this.state.toastList}
position={'bottom-right'}
/>
<div style={{display:'flex', justifyContent:'center'}}>
<Button onClick={() => this.toastSuccess()}>Success</Button>
<Button onClick={() => this.toastInfo()}>Info</Button>
<Button onClick={() => this.toastWarning()}>Warning</Button>
<Button onClick={() => this.toastDanger()}>Danger</Button>
</div>
{// ...}
</div>
);
}
Let me know if there's a way to get this code running here on StackOverflow using that Code Snippet feature, as that would be really helpful so that you readers can see the issue first-hand. Unfortunately I've never had any luck getting it to work, but I'll keep trying for a bit to see if I can figure it out.
EDIT:
Thanks to #Cristian-FlorinCalina for recommending StackBlitz as a good shareable test environment. I've got it set up there now, here's a link:
https://react-ts-ybunlg.stackblitz.io
First problem that I see with your code is that you are keeping two sources of truth for toast list. One is passed from the parent via props, and one is the internal state list in the Toast component. This is an antipattern that can generate a lot of issues.
Second BIG issue is that you are altering the list that you receive from the parent. That is a huge antipattern in React since props are readonly -- All React components must act like pure functions with respect to their props. (since you are altering an object inside an array apparently it works for the load update but it does not work when you are trying to call splice on the list -- this is why even if you deleted the element and applied the deletion effect, when it gets updated on the parent (next render) -> it will come back without it being removed and clicking again on another toast generate button will show you the previously deleted toast as well).
I think the big problem here is that you are not using composition properly. Instead of passing the toast list to the Toast component, you should keep the list on the parent, move the map from the child inside the parent. You will have one instance of Toast component per each element in the list.
Maybe you can have a ToastList component as well, that handles Toast compoonents based on their position... So when you click on Upper Left Toast Generator for example, it will add a new entry inside an array of toasts, with a position key. That array will be sent to the ToastList component, which will generate Toast components that handle their state internally (deletion, etc) and do not update the actual list. You can pass a function to the Toast component called onDelete that will be called by the Toast component on deletion, and you will update the ToastList state based on those events (probably propagate the delete event to the parent to update the list there).
Hope it makes sense.
I would like to open and close overlay using single button, so when the button is clicked an additional class is added, when closed the class is removed and overlay is closed.
So far I wrote the code that opens overlay and add/remove the class to the button.
Also I've created the method to close the overlay but I'm struggling to create a proper event to actually close it, so I would be happy if anyone can guide me a bit.
I think there should be an 'if' statement within the events() checking if the button have added class, if so, the overlay will be closed using this function element.classList.contains("active");
Also the button is animated, so when class is added 3 bars (hamburger icon) becomes X and this is the main reason I don't want to have separate buttons to open and close, I already achieved that but this is not what I'm looking for.
class OverlayNav {
constructor() {
this.injectHTML()
this.hamburgerIcon = document.querySelector(".menu-icon")
this.events()
}
events() {
this.hamburgerIcon.addEventListener("click", () => this.overlayOpen())
}
overlayOpen() {
document.getElementById("myNav").style.width = "100%";
this.hamburgerIcon.classList.toggle("menu-icon--close-x")
}
overlayClose() {
document.getElementById("myNav").style.width = "0%";
}
injectHTML() {
document.body.insertAdjacentHTML('beforeend', `
<div id="myNav" class="overlay">
<p>My Overlay</p>
</div>
`)
}
}
export default OverlayNav
You can make a function with a if statement handle Opening and closing the overlay
Here is your code edited
class OverlayNav {
constructor() {
this.injectHTML();
this.hamburgerIcon = document.querySelector(".menu-icon");
this.events();
}
events() {
this.hamburgerIcon.addEventListener("click", () => this.overlayHandle());
}
overlayOpen() {
document.getElementById("myNav").style.width = "100%";
this.hamburgerIcon.classList.toggle("menu-icon--close-x");
}
overlayClose() {
document.getElementById("myNav").style.width = "0%";
}
overlayHandle() {
if (element.classList.contains("active")) {
this.overlayClose();
} else {
this.overlayOpen();
}
}
injectHTML() {
document.body.insertAdjacentHTML(
"beforeend",
`
<div id="myNav" class="overlay">
<p>My Overlay</p>
</div>
`
);
}
}
export default OverlayNav;
You can add a property that keeps track of the state of the nav bar.
constructor() {
this.injectHTML()
this.hamburgerIcon = document.querySelector(".menu-icon")
this.events()
this.overlayVisible=true;
}
Then add a method that toggles the state and calls the right open/close-method:
toggleOverlay() {
if (this.overlayVisible)
this.overlayOpen();
else
this.overlayClose();
this.overlayVisible=!this.overlayVisible;
}
Finally make the events method call toggleOverlay() instead of overlayOpen().
events() {
this.hamburgerIcon.addEventListener("click", () => this.toggleOverlay())
}
Alternativly, a pure HTML + CSS solution, using only the details element and the [open] CSS attribute selector.
.overlay > p {
padding: 1rem;
margin: 0;
display: flex;
flex-direction: column;
width: 25vw
}
.overlay summary {
padding: 1rem 0.5rem;
cursor: pointer;
max-height: 90vh;
overflow: auto;
font-size: 4em;
list-style: none;
}
.overlay[open] summary {
background: black;
color: white;
padding: 0.5rem;
font-size: 1em;
}
.overlay[open] {
position: fixed;
/* top: calc(50% - 25vw); */
left: calc(50% - 15vw);
outline: 5000px #00000090 solid;
border: 5px red solid;
border-radius: 0.5rem;
font-size: 1em
}
.overlay[open] summary::after {
content: '❌';
float: right;
}
<details class="overlay">
<summary>☰</summary>
<p>
Hello world!
</p>
</details>
Is there any way to handle how CSS is applied to a web component like you can do with attributes using attributeChangedCallback.
I am working on a couple web components that would benefit from being styled with CSS classes, but I need to change multiple styles for it to look correct (e.g. if you set the color of the control, the user would expect the border color of one element and the font color of another to change in the shadow DOM).
Is there any way to get .usingCSS { color: red; } to change the color of the toggle switch in the following simple web component example?
// based on https://www.w3schools.com/howto/howto_css_switch.asp
class W3schoolsToggleSwitch extends HTMLElement {
constructor() {
super();
var shadow = this.attachShadow({ mode: "open" });
this.span = document.createElement("span");
this.span.innerHTML = `
<style>
/* The switch - the box around the slider */
.switch {
--color: #2196F3;
position: relative;
display: inline-block;
width: 60px;
height: 34px;
}
/* Hide default HTML checkbox */
.switch input {
opacity: 0;
width: 0;
height: 0;
}
/* The slider */
.slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #ccc;
-webkit-transition: .4s;
transition: .4s;
}
.slider:before {
position: absolute;
content: "";
height: 26px;
width: 26px;
left: 4px;
bottom: 4px;
background-color: white;
-webkit-transition: .4s;
transition: .4s;
}
input:checked + .slider {
background-color: var(--color);
}
input:focus + .slider {
box-shadow: 0 0 1px #2196F3;
}
input:checked + .slider:before {
-webkit-transform: translateX(26px);
-ms-transform: translateX(26px);
transform: translateX(26px);
}
/* Rounded sliders */
.slider.round {
border-radius: 34px;
}
.slider.round:before {
border-radius: 50%;
}
</style>
<label class="switch">
<input type="checkbox" checked>
<span class="slider round"></span>
</label>
`;
shadow.appendChild(this.span);
}
static get observedAttributes() {
return ["color"];
}
attributeChangedCallback(name, oldValue, newValue) {
console.log(name, newValue);
if ("color" === name) {
this.shadowRoot
.querySelector(".switch")
.style.setProperty("--color", newValue);
}
}
get color() {
return this.getAttribute("color");
}
set color(value) {
return this.setAttribute("color", value);
}
}
customElements.define("w3schools-toggle-switch", W3schoolsToggleSwitch);
.usingCSS {
color: red;
}
default:
<w3schools-toggle-switch></w3schools-toggle-switch>
<br><br> color attribute used to change the color to green:
<w3schools-toggle-switch color="green"></w3schools-toggle-switch>
<br><br> can you change the color with CSS?:
<w3schools-toggle-switch class="usingCSS"></w3schools-toggle-switch>
From the outside with <link>
You could apply CSS style to a Web Component uning a <link> element in the Shadow DOM.
#shadow-root
<link rel="stylesheet" href="default.css">
attributeChangedCallback( name, old, value ) {
if (name === 'class')
this.shadowRoot.querySelector( 'link' ).href = value + ".css"
}
With style defined inside Shadow DOM :host() pseudo-class function
You can apply different styles based on the context. You can combine multiple classes.
customElements.define( 'custom-element', class extends HTMLElement {
constructor() {
super()
this.attachShadow( { mode: 'open' } )
.innerHTML = `
<style>
:host( .red ) { color: red }
:host( .blue ) { color: blue }
:host( .border ) { border: 1px solid }
</style>
Hello`
}
} )
ce1.onclick = ev => ev.target.classList.add( 'border' )
<custom-element class="red" id="ce1"></custom-element>
<custom-element class="blue border"></custom-element>
On Chrome / Opera: with Constructable stylesheets
Create one (or several) Stylesheet(s) and apply it(them) to the Shadow DOM. You can apply multiple stylesheets to the same Shadow DOM.
var ss = []
ss['red'] = new CSSStyleSheet
ss.red.replaceSync( 'span { color: red }' )
ss['green'] = new CSSStyleSheet
ss.green.replaceSync( 'span { color: green }' )
ss['border'] = new CSSStyleSheet
ss.border.replaceSync( 'span { border: 1px solid }' )
customElements.define( 'custom-element', class extends HTMLElement {
constructor() {
super()
this.attachShadow( { mode: 'open' } )
.innerHTML = `<span>Hello</span>`
}
static get observedAttributes() { return [ 'class' ] }
attributeChangedCallback() {
this.shadowRoot.adoptedStyleSheets = [ ...this.classList ].map( cl => ss[ cl ] )
}
} )
ce1.onclick = ev => ev.target.classList.add( 'border' )
<custom-element class="red" id="ce1"></custom-element>
<custom-element class="green border"></custom-element>
Extending on Supersharps answer.
when you can not use Constructable Stylesheets yet:
You could (brutally) import a whole STYLE definition from the Host document.
onload=this.disabled=true to prevent styling the document DOM
or create a <my-themes></my-themes> Component that hosts (and serves) the STYLE elements
customElements.define( 'custom-element', class extends HTMLElement {
constructor() {
super()
this.root=this.attachShadow( { mode: 'open' } );
this.root.innerHTML = `<style>div{font-size:40px}</style>`
+`<style id="theme"></style><div>Click Me</div>`;
let themes = window.themes;//duplicate IDs create a global NodeList
let themeNr = 0;
this.root.addEventListener('click', ev =>
this.theme = themes[ themeNr<themes.length ? themeNr++ : themeNr=0 ].innerHTML);
}
set theme(css){
this.root.getElementById('theme').innerHTML = css;
}
} )
<style id="themes" onload="this.disabled=true">
div{
background:yellow;
}
</style>
<style id="themes" onload="this.disabled=true">
div{
background:hotpink;
font-size:30px;
}
</style>
<style id="themes" onload="this.disabled=true">
div{
background:red;
color:white;
}
div::after{
content:" theme2"
}
</style>
<custom-element></custom-element>
<custom-element></custom-element>
<custom-element></custom-element>
<div>Main Document</div>
I'm wondering if it's possible to on each appendTo make the new div unique but still use the same jquery.
As you can see in the mark-up below, each new div shares the same jquery so doesn't work independently.
Within my Javascript i'm selecting the ID to fire each function.
I've tried just adding + 1 etc to the end of each ID, but with that it changes the name of the ID making the new created DIV not function.
I've thought of using DataAttribues, but i'd still have the same issue having to create multiple functions all doing the same job.
Any ideas?
Thanks
$(function() {
var test = $('#p_test');
var i = $('#p_test .upl_drop').length + 1;
$('#addtest').on('click', function() {
$('<div class="file-input"><div class="input-file-container upl_drop"><label for="p_test" class="input-file-trigger">Select a file...<input type="file" id="p_test" name="p_test_' + i + '" value=""class="input-file"></label></div><span class="remtest">Remove</span><p class="file-return"></p></div>').appendTo(test);
i++;
});
$('body').on('click', '.remtest', function(e) {
if (i > 2) {
$(this).closest('.file-input').remove();
i--;
}
});
});
var input = document.getElementById( 'file-upload' );
var infoArea = document.getElementById( 'file-upload-filename' );
input.addEventListener( 'change', showFileName );
function showFileName( event ) {
// the change event gives us the input it occurred in
var input = event.srcElement;
// the input has an array of files in the `files` property, each one has a name that you can use. We're just using the name here.
var fileName = input.files[0].name;
// use fileName however fits your app best, i.e. add it into a div
textContent = 'File name: ' + fileName;
$("#input-file-trigger").text(function () {
return $(this).text().replace("Select a file...", textContent);
});
}
/*
#### Drag & Drop Box ####
*/
.p_test{
display: inline-block;
}
.upl_drop{
border: 2px dashed #000;
margin: 0px 0px 15px 0px;
}
.btn--add p{
cursor: pointer;
}
.input-file-container {
position: relative;
width: auto;
}
.input-file-trigger {
display: block;
padding: 14px 45px;
background: #ffffff;
color: #1899cd;
font-size: 1em;
cursor: pointer;
}
.input-file {
position: absolute;
top: 0; left: 0;
width: 225px;
opacity: 0;
padding: 14px 0;
cursor: pointer;
}
.input-file:hover + .input-file-trigger,
.input-file:focus + .input-file-trigger,
.input-file-trigger:hover,
.input-file-trigger:focus {
background: #1899cd;
color: #ffffff;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>
<div class="p_test" id="p_test">
<div class="file-input">
<div class="input-file-container upl_drop">
<input class="input-file" id="file-upload" type="file">
<label tabindex="0" for="file-upload" id="input-file-trigger" class="input-file-trigger">Select a file...</label>
</div>
<div id="file-upload-filename"></div>
</div>
<button class="btn--add" id="addtest">
Add
</button>
</div>
I'd advise against using incremental id attributes. They become a pain to maintain and also make the logic much more complicated than it needs to be.
The better alternative is to use common classes along with DOM traversal to relate the elements to each other, based on the one which raised any given event.
In your case, you can use closest() to get the parent .file-input container, then find() any element within that by its class. Something like this:
$(function() {
var $test = $('#p_test');
$('#addtest').on('click', function() {
var $lastGroup = $test.find('.file-input:last');
var $clone = $lastGroup.clone();
$clone.find('.input-file-trigger').text('Select a file...');
$clone.insertAfter($lastGroup);
});
$test.on('click', '.remtest', function(e) {
if ($('.file-input').length > 1)
$(this).closest('.file-input').remove();
}).on('change', '.input-file', function(e) {
if (!this.files)
return;
var $container = $(this).closest('.file-input');
$container.find(".input-file-trigger").text('File name: ' + this.files[0].name);
});
});
.p_test {
display: inline-block;
}
.upl_drop {
border: 2px dashed #000;
margin: 0px 0px 15px 0px;
}
.btn--add p {
cursor: pointer;
}
.input-file-container {
position: relative;
width: auto;
}
.input-file-trigger {
display: block;
padding: 14px 45px;
background: #ffffff;
color: #1899cd;
font-size: 1em;
cursor: pointer;
}
.input-file {
position: absolute;
top: 0;
left: 0;
width: 225px;
opacity: 0;
padding: 14px 0;
cursor: pointer;
}
.input-file:hover+.input-file-trigger,
.input-file:focus+.input-file-trigger,
.input-file-trigger:hover,
.input-file-trigger:focus {
background: #1899cd;
color: #ffffff;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>
<div class="p_test" id="p_test">
<div class="file-input">
<div class="input-file-container upl_drop">
<input class="input-file" type="file">
<label tabindex="0" for="file-upload" class="input-file-trigger">Select a file...</label>
</div>
<div class="file-upload-filename"></div>
</div>
<button class="btn--add" id="addtest">Add</button>
</div>
Note that I've made a couple of other optimisations to the code. Firstly it now makes a clone() of the last available .file-input container when the Add button is clicked. This is preferred over writing the HTML in the JS file as it keeps the two completely separate. For example, if you need to update the UI, you don't need to worry about updating the JS now, as long as the classes remain the same.
Also note that you were originally mixing plain JS and jQuery event handlers. It's best to use one or the other. As you've already included jQuery in the page, I used that as it makes the code easier to write and more succinct.
Finally, note that you didn't need to provide a function to text() as you're completely over-writing the existing value. Just providing the new string is fine.
I am having some issues with React JS rendering children when rendering the parent.
Here I am trying to implement the "Game of Life" (a project from Freecodecamp class).
I am stuck in this situation. I click on a dead cell and it becomes alive (blue). Then, suppose I want to clear the grid, that is, make all cells dead, but it doesn't work. It seems that even re-rendering the parent will not re-render the children.
Any idea?
var board = [];
var width = 80;
var length = 50;
var cells = width * length;
var states = ["alive", "dead"];
class BoardGrid extends React.Component {
constructor(props) {
super(props);
//this.initBoardArray = this.initBoardArray.bind(this);
}
render() {
//this.initBoardArray();
let boardDOM = this.props.board.map(function(cell) {
return <BoardGridCell status={cell.status} id={cell.id} />;
});
return (
<div id="game-grid">
{boardDOM}
</div>
);
}
}
class BoardGridCell extends React.Component {
render() {
return (
<div
id={this.props.id}
className={`cell ${this.props.status}`}
data-status={this.props.status}
/>
);
}
}
function initBoard() {
for (let cellIndex = 0; cellIndex < cells; cellIndex++) {
let cell = { id: cellIndex, status: "dead" };
board[cellIndex] = cell;
}
}
function drawBoard() {
ReactDOM.render(
<BoardGrid board={board} />,
document.getElementById("game-grid-wrapper")
);
}
function clearBoard() {
for (let cellIndex = 0; cellIndex < cells; cellIndex++) {
let cell = { id: cellIndex, status: "dead" };
board[cellIndex] = cell;
}
}
$("#game-grid-wrapper").on("click", ".cell", function() {
let currentState = $(this).attr("data-status");
let currentStateIndex = states.indexOf(currentState);
let newState = states[(currentStateIndex + 1) % 2];
$(this).attr("class", `cell ${newState}`);
$(this).attr("data-status", newState);
});
$("#stop").on("click", function() {
alert("clearing");
clearBoard();
drawBoard();
});
initBoard();
drawBoard();
html,
body {
height: 100%;
text-align: center;
font-family: 'Open Sans', sans-serif;
}
h1,
h2 {
font-family: 'Press Start 2P', cursive;
}
.button {
width: 100px;
border: 1px solid #555;
padding: 5px;
margin: 5px;
cursor: pointer;
border-radius: 4px;
}
.button:hover {
opacity: 0.9;
}
#main {
margin: 10px;
}
#game-grid {
background-color: #000;
display: flex;
flex-wrap: wrap;
align-content: flex-start;
width: 800px;
height: 500px;
overflow: hidden;
}
#game-grid .cell {
border: 1px solid #767676;
width: 10px;
height: 10px;
color: #fff;
font-size: 9px;
box-sizing: border-box;
}
.alive {
background-color: #2185d0;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div id="main">
<div id="game-actions">
<div id="start" class="button"><i class="fa fa-play"></i> Start</div>
<div id="pause" class="button"><i class="fa fa-pause"></i> Pause</div>
<div id="stop" class="button"><i class="fa fa-stop"></i> Stop</div>
</div>
<div id='game-grid-wrapper'></div>
</div>
You should not use jQuery together with React if you don't have to. Both manipulate the DOM but based on different information which can make them interfere in an unexpected way.
For your board state you should use the state of your BoardGrid component. Initialize your state in the constructor and add an onClick() callback to each cell when rendering it.
When the cell is clicked call the callback function given by the board component and pass it's id with it. Use that id to update the board state using setState() in your BoardGrid component.
I can add some example code later, if you struggle with anything.