In React, I'm attempting to change the display of 2 elements when certain buttons are clicked on. I either want the title displayed, or an input to edit the title. My title div has a className of a variable named 'titleDisplay' with the value of 'isDisplayed'. I have a function set up to switch it to 'notDisplayed'
let titleDisplay = 'isDisplayed';
let editDisplay = 'notDisplayed';
const editDisplayed = () => {
editDisplay = 'isDisplayed'
titleDisplay = 'notDisplayed'
}
<button onClick={editDisplayed} id="edit-btn">EDIT</button>
<div className={titleDisplay}>
<h2 className='indieNotebookTitle'>{notebook?.title}</h2>
</div>
.isDisplayed {
display: block;
}
.notDisplayed {
display: none;
}
Not sure what I'm doing wrong since onClick has been working with functions up until now.
React will only re-render a component in response to a state change. Reassigning a variable, by itself, almost never has any side-effects, unless the variable is used later. Your reassignment of editDisplay would only be visible to other functions within the same component binding (a very unusual occurrence to see in React, and usually an indicator of a logic problem).
You need to make the variables you try to change stateful instead. Also, consider using a boolean value rather than a string value. You may be able to use only a single state value instead, to toggle between the title and the edit.
const [editingTitle, setEditingTitle] = useState(false);
<button onClick={() => setEditingTitle(false)} id="edit-btn">EDIT</button>
<div className={editingTitle ? 'isDisplayed' : 'notDisplayed'}>
<h2 className='indieNotebookTitle'>{notebook?.title}</h2>
</div>
Related
I have a modal that becomes visible via a JS trigger
<div x-show="modalVisible" x.on:click.away="modalVisible = false" id="modal">
</div>
document.getElementById("modal").setAttribute("style", "display: show;")
The problem I am facing is, if I use JS to make the modal visible x.on:click.away does not make the model hidden.
What am I doing wrong?
Disclaimer: I'm not by any kind of an expert in JS. Just a noob. So, sorry if I theorize it wrongly.
When you set element.style.display = "block" you're setting the property of modalVisible = false (as opposed to toggling the property like modalVisible = !modalVisible) as in your #click.away expression.
However, you have triggered the Alpine's event listener. Alpine will then continue listening to your #click.away event, and set modalVisible = false every time you click outside of the component.
I've played with this kind of situation on my Codepen (with some console.logs to debug), and it will work if I set #click.away ="modalVisible = !modalVisible" that toggles the state rather than sets the state like #click.away = "modalVisible = false". I write the property as show instead of modalVisible:
https://codepen.io/wanahmadfiras/pen/KKNrXOO
First of all, there is no such thing as display: show, you'd have to set the initial display you had, so perhaps display: block could work.
AlpineJS has a similar example in their github repository, so I just adapted it a little to fit your needs.
<div id="modal" x-data="{ modalVisible: false }">
<button #click="modalVisible = true">Open Modal</button>
<ul x-show="modalVisible" #click.away="modalVisible = false">
Modal Content
</ul>
</div>
I found a way to make this work using a custom event instead of removing display: none with custom JS.
let event = new CustomEvent("modal", {
detail: {
items: []
}
});
window.dispatchEvent(event);
In my HTML I added
x-on:modal.window="modalVisible = true"
This works though I still don't know why altering directly with JS does not work.
UPDATE
Here's are some demos
contentEditable demo - requires double click for H1 to become editable
replace with input demo - adopts event.target styles but makes the UI 'twitch' when rendered
So I have some functional components, let's say:
component1.js
import React from 'react';
const component1 = props => (
<div>
<h1>Title</h1>
</div>
);
export { component1 };
They are variable. event.target could be anything with text, so paragraph, heading, anything. I'm trying to let users edit content inline by clicking on it, so I'll pass a function editMode to these functional components, that'll update parent state with editing info, let's say like this:
<h1 onClick={event => {editMode(event, props.name, props.title, 'title')}}>title</h1>
This changes parent local state to have all the necessary information to grab the value from redux, define a target etc. For this example, props.name is the name of the component, props.title is the value, and 'title' is object key in redux.
So I'll add something to my component1.js and make it look a bit like this:
import React from 'react';
const component1 = props => (
<div>
{props.editState === 'true' &&
<EditLayout
name={props.name}
target={props.target}
value={props.value}
onChange={event => someFunc(event)}
/>
}
<h1>Title</h1>
</div>
);
export { component1 };
Now this works fine, except it doesn't scale. EditLayout, in this case, will just return an input with correct value. What I need it to do is to adapt to whatever is being clicked, get font size, background, padding, margin, position. Am I doing this right? Every way I try, I run into huge issues:
Idea 1 - move EditLayout component outside of the functional component
Issue: positioning
So I'll move EditLayout to parent component that contains both component1.js and EditLayout. This will allow me to manipulate it from inside the functional component, without having to include it everywhere. I'll then grab coordinates and other important information from event.target like so:
const coords = event.target.getBoundingClientRect();
const offsetX = coords.left;
const offsetY = coords.top;
const childHeight = coords.height;
const childWidth = coords.width;
const childClass = event.target.className;
I'll then wrap the EditLayout to return a container which contains an input, and apply size/coordinates to the absolutely positioned container. This'll present an issue of input being offset by a random amount of pixels, depending on how big/where is the event.target.
Idea 2 - pass relevant computed styles to EditLayout
Issue: twitching on render, and I have to add EditLayout for every possible event.target there is, as well as condition its' render
So I'll grab all important computed styles like this:
const computedTarget = window.getComputedStyle(event.target);
const childMargins = computedTarget.marginBottom;
const childPaddings = computedTarget.padding;
const childFontSize = computedTarget.fontSize;
const childTextAlign = computedTarget.textAlign;
And pass it to component1.js, and then pass it to EditLayout component inside the component1.js. I'll then condition theevent.target to hide if it's being edited like this:
<h1 className={ props.target === 'title' ? 'd-none' : ''}>Title</h1>
And condition the EditLayout to show only if it's needed:
{props.target === 'title' && <EditLayout />}
In this example, clicking h1 will show the input, but the layout itself with twitch on render. Input will have the exact same margin and font size as the h1, or event.target, but it'll appear bigger and extend the layout. Demo:
Idea 3 - Use conditional contentEditable
Issue: Requires double click to enable, doesn't work in safari, doesn't let me preselect the value
This is the weirdest of them all. I figured it'd be pretty simple, do something like this inside the functional component render:
<h1 contentEditable={props.target === 'title'} onClick={event => props.setTarget(event)}>Title</h1>
However, I have to double click to enable it. I have no idea why, if I attach a console log every time onClick is fired, I'll get correct outputs, I'll get the correct target value as well. I've tried numerous ways, but it simply requires double click. Even attempted to handle this inside the functional component, as most of the stuff is handled by a parent component, doesn't make a difference.
I have oversimplified the examples, so it's safe to assume/understand the following:
I am passing props in a correct fashion, they aren't undefined
I am using bootstrap
I am using styled components, and EditLayout is a styled component
which accepts props and turns them into CSS like: font-size: ${props
=> props.fontSize};
The values should be correct, I am not manipulating anything I get back from getComputedStyle() or getBoundingClientRect()
I am keen on keeping my functional components functional, and easy to
add. Functional components, in this case, are simple HTML structures,
and I'd like to keep them as simple as possible
So there's a neat solution to contentEditable requiring two clicks instead of one, instead of binding onClick and passing it to enable contentEditable, simply keep contentEditable true and handle the change however you like. Here's a working h1 that doesn't require two clicks to enable contentEditable, unlike the one in the demo
<h1
className="display-4 text-center"
contentEditable
suppressContentEditableWarning
onBlur={event => updateValues(event)}
>
Title
</h1>
The available methods for trigger update could be onBlur or onInput.
I am forcing element to be focused like this
/**focusing the element if the element is active */
componentDidUpdate() {
console.log(this.activeElementContainer);
if(this.activeElementContainer!==undefined && this.activeElementContainer!==null) {
/** need to focus the active elemnent for the keyboard bindings */
this.activeElementContainer.focus();
}
}
My render has conditional rendering the elements are being rendered dynamically from the array,
Let say I have one element in div and I am adding another from the toolbox. In that case I need to focus the last element I dragged.
render() {
let childControl= <span tabIndex="-1" dangerouslySetInnerHTML={{__html: htmlToAdd}}></span>;
if(this.props.activeItem){
childControl=<span ref={ (c) => this.activeElementContainer = c }
tabIndex="0" dangerouslySetInnerHTML={{__html: htmlToAdd}}></span>
}
//later I ma using childControl to array and it works fine.
The logs says, first time it works fine
But, second time the this.activeElementContainer is undefined
Is there any alternative way or possible solution to this?
The thing is I need to focus only one element at the time.
Remember: Activecontrol has too many things to do like it can have right click menu, drag etc. so, I need to render it separately.
After reading this one github:
This is intended (discussed elsewhere) but rather unintuitive
behavior. Every time you render:
<Value ref={(e) => { if (e) { console.log("ref", e); }}} /> You are
generating a new function and supplying it as the ref-callback. React
has no way of knowing that it's (for all intents and purposes)
identical to the previous one so React treats the new ref callback as
different from the previous one and initializes it with the current
reference.
PS. Blame JavaScript :P
Source
I changed my code to
<span id={this.props.uniqueId} ref={(c)=>
{if (c) { this.activeElementContainer=c; }}
} tabIndex="0" dangerouslySetInnerHTML={{__html: htmlToAdd}}></span>
Adding if was the real change. now, it has a ref.
For others who face this problem:
I need to write custom function too, in the componentDidUpdate I am still getting old reference,
ref={(c)=>{if (c) { this.activeElementContainer=c; this.ChangeFocus(); }}
Adding this was the perfect solution for me
Summary
When a user clicks the hamburger icon className='.landing-page-hamburger' it takes two clicks to toggle the navigation className='landing-page-nav-links' and I can't figure out why.
The display value for .landing-page-nav-links is set to none by default in the CSS.
Problem
Navigation bar is requiring two clicks to toggle the display value
Expected Result
Would expect to only need one click
LandingPage.js
import React from 'react';
const LandingPage = () => {
const toggleNav = () => {
const navLinks = document.querySelector('.landing-page-nav-links');
if (navLinks.style.display === 'none') {
navLinks.style.display = 'initial';
} else {
navLinks.style.display = 'none';
}
}
return (
<nav className='landing-page-nav'>
<h1 className='take-notes-logo'>Take Notes</h1>
<span className='landing-page-hamburger' onClick={() => toggleNav()}>☰</span>
<ul className='landing-page-nav-links'>
<li>Sign Up</li>
<li>Log In</li>
</ul>
</nav>
);
};
export default LandingPage;
This is happening because your external CSS is not setting a style property on your element. The first click sets it to none because it isn't there at all. Your second click will now work as expected.
To fix this, either set the style of .landing-page-hamburger inline, or just toggle classes and let your external CSS handle it.
EDIT: The OP asks an excellent question. The .style property refers to the element's CSSStyleDeclaration object. This is a HTMLElement interface that you are directly manipulating. Notice, when you inspect an element in your console, the CSSStyleDeclaration does not necessarily match what you see in the CSS you get from the style sheet. Also, note that the properties are JS-style camelCase; this illustrates that this is not a direct mapping, yet they both effect the element.
So my initial description of the property being not there at all isn't accurate. It's there, and it's set to the empty string, which is the default CSSStyleDeclaration. So you set it to "none" on the second click and all goes well from there. Your initial CSS declaration is working correctly, but not figuring into your if statement because it's not referring to the same thing. Same effect, but you're interfacing with your element in a different way.
I am using Template.registerHelper to register a helper that would given some boolean value will output either class1 or class2 but also some initial class if and only if it was the first time it was called for this specific DOM element.
Template.registerHelper('chooseClassWithInitial', function(bool, class_name1, class_name2, initial) {
var ifFirstTime = wasICalledAlreadyForThisDOMElement?;
return (ifFirstTime)?initial:"" + (bool)?class_name1:class_name2;
});
I am having a hard time figuring out how to know if the helper was called already for this specific form element.
If I could somehow get a reference to it, I could store a flag in the data attribute.
Using Template.instance() one can get to the "template" instance we are now rendering and with Template.instance().view to the Blaze.view instance, however, what if we have more than one html element inside our template ?
Oh, you are doing it in the wrong direction.
If you want to manipulate the DOM, you should do it directly in the template, not the jquery way ;)
0. Helper
html
<template name="foo">
<div data-something="{{dataAttributeValue}}"></div>
</template>
js
Template.foo.helpers({
dataAttributeValue: function() {
return 'some-value';
}
})
If you cannot avoid accessing the DOM from outside, then there is Template.onRendered(callback), callback will be called only once, when the template is rendered for the first time.
1. Component style
<template name="fadeInFadeOut">
<div class="fade">{{message}}</div>
</template>
Template.onRendered(function() {
// this.data is the data context you provide
var self = this,
ms = self.data.ms || 500;
self.$('div').addClass('in'); // self.$('div') will only select the div in that instance!
setTimeout(function() {
self.$('div').removeClass('in')
self.$('div').addClass('out')
}, ms );
});
Then you can use it somewhere else:
<div>
{{>fadeInFadeOut message="This message will fade out in 1000ms" ms=1000 }}
</div>
So you would have a reusable Component..
The way I solved it for now was to manually provide some kind of global identifier, unique to that item, this is hardly the proper way, if anyone has suggestions let me know.
let chooseClassWithInitialDataStore = {};
Template.registerHelper('chooseClassWithInitial', function(bool, class_name1, class_name2, initial, id) {
if(!chooseClassWithInitialDataStore[id]){
chooseClassWithInitialDataStore[id] = true;
return initial;
}
return (bool)?class_name1:class_name2;
});
To be used like:
<div class="ui warning message lowerLeftToast
{{chooseClassWithInitial haveUnsavedChanges
'animated bounceInLeft'
'animated bounceOutLeft'
'hidden' 'profile_changes_global_id'}}
">
Unsaved changes.
</div>
Regarding this specific usage: I want to class it as 'animated bounceInLeft' haveUnsavedChanges is true, 'animated bounceOutLeft' when its false, and when it is first rendered, class it as 'hidden' (that is, before any changes happen, so that it doesnt even display when rendered, thus, the need for the third option, however this isnt a question about CSS, but rather about Meteor templateHelpers).