I want to make a dialog element to edit events (an edit button is placed with each event) in a separate component. Is there a built-in way I can do this in svelte? I tried changing the display from none to block and vice versa but it keeps telling me 'dialog is null'. All of the youtube tutorials in this are too outdated, the packages do not exist on GitHub anymore.
While we're at it, is this the best practice for my situation or can it be improved?
I tried fixing the error with this code, the function is triggered but nothing happens
<script>
let dialogRef;
let showDialog = false;
const editEvent = () => {
if (dialogRef) {
dialogRef.showModal();
showDialog = true;
}
};
const closeDialog = () => {
dialogRef.close();
showDialog = false;
};
</script>
<main>
{#if showDialog}
<dialog on:cancel={closeDialog} bind:this={dialogRef}>
<button class="close" on:click={closeDialog}>x</button>
<p>content here</p>
</dialog>
{/if}
<button on:click={editEvent}>Edit Event</button>
</main>
<style>
dialog {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 9999;
}
</style>
While showDialog is false, the element does not exist because of the #if. To make sure that the element is always there, the easiest thing to do is not using #if but a conditional class, e.g. class:hidden={!showDialog}, which sets display to none. Though you should not need that at all, as the dialog should not be visible by default until its methods are called.
REPL
Another approach would be to change the order of operations and wait for the element to be created.
const editEvent = async () => {
showDialog = true;
await tick(); // from 'svelte', waits for the UI to update
dialogRef.showModal();
};
REPL
Related
I am creating a list of divs, which was created with map.
function renderButtons(){
const options = [...Array(10).keys()] // returns [0,1,2...9]
return _.map(options, (option)=> renderOption(option))
}
function renderOption(option:number){
return (
<div className="option-container" onClick={() => setLowerContainerVisible(true)}>
<img alt="" src={"./images/feedback-icons/icon-"+option.toString()+".svg"}/>
{option+1}
</div>
)
}
this renders a list of divs, and I was able to change each div background, when hover, like this:
.option-container{
width: 76px;
height: 100px;
background-color: #7777ff;
display: flex;
}
.option-container:hover{
background-color: #adadf3;
}
I wish to be able to click on a div, and change its background color to white. everything I try will change the background of all the 10 divs to white. How can I make it so only the clicked one is changed?
I suggest that you use renderOption and renderButtons as two components rather than plain functions. In the RenderButtons component, you can use some state to maintain which item is clicked, and within RenderOption you can control whether the background color is white or not based on wehther or not the current rendered button is the clicked option. In your .map() method, you can use component rather than a function call <RenderOption option={option} ... />.
See example below:
const {useState} = React;
function RenderButtons() {
const [clickedItem, setClickedItem] = useState(-1);
return Array.from(
Array(10).keys(),
option => <RenderOption isClicked={clickedItem === option} option={option} setClicked={setClickedItem}/>
);
}
function RenderOption({isClicked, option, setClicked}) {
const handleClick = () => {
// setLowerContainerVisible(true) / other code to run when you click
setClicked(option); // set to current option
}
return (
<div className={"option-container " + (isClicked ? "clicked" : "")} onClick={handleClick}>
{option+1}
</div>
)
}
ReactDOM.render(<RenderButtons />, document.body);
.option-container {
width: 76px;
height: 100px;
background-color: #7777ff;
display: flex;
}
.option-container.clicked, .option-container:hover {
background-color: #adadf3;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.0/umd/react-dom.production.min.js"></script>
The className is a little messy as it involves a ternary, to clean this up it might be worth looking into using a node package such as classnames which allows you to easily build a list of classes based on conditions.
Do it in the event listener function:
<div className="option-container" onClick={highlightAndsetLowerContainerVisible}>
function highlightAndsetLowerContainerVisible(event){
event.preventDefault();
setLowerContainerVisible(true)
event.currentTarget.style.backgroundColor = "red";
}
You then might also want to reset the background color of the other divs
I have dynamic list of 'posts' where I wanted to truncate the text (if it goes beyound a certain # of lines) and show a Read More button that users can click to show the entire text.
In VueJS, I decided to attach a ref to the div I want to append the button to (if the text is truncated).
The component is just a button really but it has some stylings and behaviors I want to copy over. The reason why this got more complicated then it needs to (bad thing?) is because I'm doing the truncating with CSS. I understand that using Javascript might have been easier.
So anyways, how can I dynamically add a component to this div (or its parent) using Javascript only? My own reference to the location would be the ref item.
// code after the promise of getting the posts has resolved in the created() hook
.then(() => {
const posts = this.$refs.posts
posts.forEach(p => {
if (this.Overflown(f)) {
// I want to attach a component (AwesomeButtonComponent) to this p div.
}
}
})
And for clarity:
HTML:
<div v-for="post in posts">
<div class="postBody ref="posts">{{ post.body }}</div>
</div>
isOverflown(el) {
return el.scrollHeight > el.clientHeight || el.scrollWidth > el.clientWidth;
}
The CSS that is truncating the text
.postsBody {
white-space: pre-line;
display: -webkit-box;
-webkit-line-clamp: 5;
-webkit-box-orient: vertical;
overflow: hidden;
}
ALTERNATIVE POSSIBILITIES:
This button will only have ONE functionality, so it being a component is not important and adding styling isn't so difficult.
.then(() => {
const announcementBodies = this.$refs.announcementBody;
announcementBodies.forEach(a => {
if (this.isOverflown(a)) {
const button = document.createElement('button');
button.innerText = 'Click Me';
button.onClick = 'doThis';
a.parentElement.appendChild(button);
}
});
In which case the difficult part would be to add a v-on:click directive to that button and then target that specific tag to remove the clamp css attribute.
Following our discussion into comments, I'll show you 2 way you can do this and try to explain the difference between them and let you decide how you will achieve this.
EXAMPLE ONE
The first example is the shortest I could do. This will need every post to have an isOverflow attribute. There is many way to do it client or server side. The other example will not need it.
<div v-for="post in posts">
{{(post.isOverflow == true) ? post.body.substring(0,3)+'...' : post.body}} <button v-on:click="post.isOverflow = !post.isOverflow">{{(post.isOverflow == true) ? 'SHOW MORE' : 'SHOW LESS'}}</button>
</div>
This is not beautiful, but it work and it let you understand that you can manipulate the post inside the v-for. Each button will be automatically associate with the right post, so when you will click it, only the post associated will be affected.
EXAMPLE TWO
The other example i'll give you is by creating a new component for each post. Let's start with the v-for:
<post-component v-for="post in posts" v-bind:key="post.id" v-bind:post="post"></post-component>
And the new component:
<template>
<div v-bind:class="{'postsBody': isOverflow}">
{{post.body}}
<button v-on:click="changeState()">{{(post.isOverflow) ? 'SHOW LESS' : 'SHOW MORE'}}</button>
</div>
</template>
<script>
export default {
props: {
post:{}
},
data() {
return {
isOverflow: true
}
},
methods: {
changeState: function() {
this.isOverflow = !this.isOverflow;
}
},
}
</script>
<style> //Please, put this in a CSS file, it's only for the example purpose.
.postsBody {
white-space: pre-line;
display: -webkit-box;
-webkit-line-clamp: 5;
-webkit-box-orient: vertical;
overflow: hidden;
}
</style>
CONCLUSION
In the end, both of them will have the same result. The difference is what you prefer. I tried to show you two different way to let you understand how things work with Vue. Let me know if you need more explanations.
i am trying to create a popupdialog to be shown when user clicks on a button. for that i am using portal.
i want it to look like in the picture below,
So basically, when user clicks on the add button i want the popup dialog to display like in the picture above.
in the popup component i want to render overlay with children. and when user clicks on overlay div the popup should close.
I have something that kind of works without using Portal and is like below,
below is my code that is without using Portal,
function Parent({isDialogOpen, setDialogOpen, setSomething}: Props) {
const [isClicked, setIsClicked] = React.useState(false);
const handleButtonClick = () => {
if (setIsDialogOpen) setIsDialogOpen(!isDialogOpen);
if (setSomething) setSomething(isDialogOpen);
setIsClicked(!isClicked);
};
return (
<button onClick={handleButtonClick}>click</button>
{isDialogOpen && isClicked &&
<Overlay>
<Dialog>
//some divs
</Dialog>
</Overlay>
}
);
}
const Overlay = styled.div`
position: fixed;
padding-top:60px;
bottom: 40px;
top: 0;
left: 0;
right: 0;
backdrop-filter: blur(8px);
z-index: 100;
display: flex;
justify-content: center;
align-items: center;
`;
const Dialog = styled.div`
padding: 16px;
width: 384px;
max-height: calc(100% - 200px);
display: flex;
flex-direction: column;
`;
Now i am rewriting above using portal like below,
function Parent({isDialogOpen, setDialogOpen, setSomething}: Props) {
const [isClicked, setIsClicked] = React.useState(false);
const handleButtonClick = () => {
if (setIsDialogOpen) setIsDialogOpen(!isDialogOpen);
if (setSomething) setSomething(isDialogOpen);
setIsClicked(!isClicked);
};
return (
<button onClick={handleButtonClick}>click</button>
{isDialogOpen && isClicked &&
<Popup setSomething={setSomething} setIsDialogOpen={setIsDialogOpen} setIsClicked=
{setIsClicked}>
<Dialog>
//some divs
</Dialog>
</Overlay>
}
);
}
function Popup({setIsClicked, setSomething, setIsDialogOpen, children}: Props) {
return ReactDom.createPortal(
<>
<Overlay
onClick={() => {
if (setIsDialogOpen) setIsDialogOpen(false);
if (setSomething) setSomething(true);
setIsClicked(false);
}}
>
{children}
</Overlay>
</>,
//dont know what to pass here
);
}
Basically as seen in picture above, i want to render the overlay with dialog.
now in popup component i want to create div with classname 'popup' and find the div element with class navbar and attach this div popup to the navbar div
and pass this div element with class popup in the reactDOM.createPortal.
i am new to react and not sure how to do this. could someone help me with this.
thanks.
As I mentioned to you in the comment, react doesn't create the dom node for you. You must do it yourself. How you do it depends on your needs. the most basic example i can think of is below:
When component mounts first time, we need to create the portal and insert it into document.body
once we are sure portal exists, we can render into the portal using our dom ref.
function Popup({setIsClicked, setSomething, setIsDialogOpen, children}: Props) {
const [portal,setPortal] = React.useState<HTMLDivElement|null>( (document.getElementById('my-portal') as HTMLDivElement)||null);
const createPortalIfNotExists = React.useCallback(()=>{
if(portal===null){
const el = document.createElement('div');
el.id='my-portal';
document.body.appendChild(el);
// switch this line for the one above if you want it to be first in tree
// document.body.insertBefore(el, document.body.firstChild);
setPortal(document.getElementById('my-portal') as HTMLDivElement);
}
},[portal]);
createPortalIfNotExists();
if(portal===null){
return null;
}
return ReactDom.createPortal(
<>
<Overlay
onClick={() => {
if (setIsDialogOpen) setIsDialogOpen(false);
if (setSomething) setSomething(true);
setIsClicked(false);
}}
>
{children}
</Overlay>
</>,
portal
);
}
This is just one possible way of doing it. There are other more advanced use cases where you would have the portal be rendered by some other element in your component tree. But this should be enough to get you started. Also, i haven't tested this as i don't have tsc/tslint on this machine so YMMV
I want an overlay to show up when I click a search icon.
I managed to get it working using jQuery. But can't seem to get it working with javascript.
The click event does not seem to be registering and I don't know why.
I've checked all the class names so they match in the same in both the HTML and javascript
Here is the jQuery code that works:
import $ from 'jquery';
class Search {
constructor() {
this.openButton = $('.js-search-trigger');
this.closeButton = $('.search-overlay__close');
this.searchOverlay = $(".search-overlay");
this.events();
}
events() {
this.openButton.on('click', this.openOverlay.bind(this));
this.closeButton.on('click', this.closeOverlay.bind(this));
}
openOverlay() {
this.searchOverlay.addClass("search-overlay--active");
}
closeOverlay() {
this.searchOverlay.removeClass("search-overlay--active");
}
}
export default Search;
Here is the javascript code that does not work:
class Search {
constructor() {
this.openButton = document.querySelector('.js-search-trigger');
this.closeButton = document.querySelector('.search-overlay__close');
this.searchOverlay = document.querySelector('.search-overlay');
this.events();
}
events() {
this.openButton.addEventListener('click', function() {
this.openOverlay.bind(this);
});
this.closeButton.addEventListener('click', function() {
this.closeOverlay.bind(this);
});
}
openOverlay() {
this.searchOverlay.classList.add('search-overlay--active');
}
closeOverlay() {
this.searchOverlay.classList.remove('search-overlay--active');
}
}
export default Search;
No errors were shown in the javascript where the overlay was not showing.
You'll probably want to change your event listeners to use the correct this binding:
this.openButton.addEventListener("click", this.openOverlay.bind(this));
Or use an arrow function to go with your approach - but make sure you actually call the resulting function, as in the above approach the function is passed as a reference and is called. If you removed the additional () from the code below, it would be the same as writing a function out in your code normally - it would be defined, but nothing would happen.
this.openButton.addEventListener("click", () => {
this.openOverlay.bind(this)();
});
jQuery also uses collections of elements rather than single elements, so if you have multiple elements, querySelectorAll and forEach might be in order.
If we are speaking of ecmascript-6 (I see the tag), I would recommend to use arrow function to have this inherited from the above scope, and no bind is needed:
this.openButton.addEventListener('click', () =>
this.openOverlay()
);
The problems with your code are that a) the function creates new scope with its own this; b) bound methods are not being invoked.
Why Search? You're creating an Overlay. Stick with the plan.
No need to bind anything. Use Event.currentTarget if you want to.
No need to handle .open/.close if all you need is a toggle.
And the below should work (as-is) for multiple Overlays. The overlay content is up to you.
class Overlay {
constructor() {
this.toggleButtons = document.querySelectorAll('[data-overlay]');
if (this.toggleButtons.length) this.events();
}
events() {
this.toggleButtons.forEach(el => el.addEventListener('click', this.toggleOverlay));
}
toggleOverlay(ev) {
const btn = ev.currentTarget;
const sel = btn.getAttribute('data-overlay');
const overlay = sel ? document.querySelector(sel) : btn.closest('.overlay');
overlay.classList.toggle('is-active');
}
}
new Overlay();
*{margin:0; box-sizing:border-box;} html,body {height:100%; font:14px/1.4 sans-serif;}
.overlay {
background: rgba(0,0,0,0.8);
color: #fff;
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
padding: 5vw;
transition: opacity 0.4s, visibility 0.4s;
visibility: hidden;
opacity: 0;
}
.overlay.is-active {
opacity: 1;
visibility: visible;
}
<button type="button" data-overlay="#search">OPEN #search</button>
<button type="button" data-overlay="#qa">OPEN #qa</button>
<div class="overlay" id="search">
<button type="button" data-overlay>CLOSE</button>
<h2>SEARCH</h2>
<input type="text" placeholder="Search…">
</div>
<div class="overlay" id="qa">
<button type="button" data-overlay>CLOSE</button>
<h2>Q&A</h2>
<ul><li>Lorem ipsum</li></ul>
</div>
The above is still not perfect, still misses a way to "destroy" events and not re-attach duplicate events to already initialised buttons when trying to target dynamically created ones.
Also, the use of Classes for the above task is absolutely misleading and unnecessary.
I was wondering if anyone knows the easiest way to create a delete(confirmation) modal in react.js? I have been playing around with a few things but cannot get my head around it.
The modal needs to pop up from a bin icon upon click. I am a complete beginner to react so I am struggling quite a bit.
Here's an example using https://github.com/GA-MO/react-confirm-alert -
yarn add react-confirm-alert
display.jsx:
import { confirmAlert } from 'react-confirm-alert'
import 'react-confirm-alert/src/react-confirm-alert.css'
const msg = `Item ${item.style} (barcode ${item.barcode}) is not
currently in this display. Do you want to add it?`
const addDialog = ({ onClose }) => {
const handleClickedNo = () => {
alert('clicked no')
onClose()
}
const handleClickedYes = () => {
alert('clicked yes')
onClose()
}
return (
<div className='add-dialog'>
<h3>Add item to display</h3>
<p>{msg}</p>
<div className="add-dialog-buttons">
<button onClick={handleClickedNo}>No</button>
<button onClick={handleClickedYes}>Yes, add item</button>
</div>
</div>
)
}
confirmAlert({ customUI: addDialog })
You can add your own custom css to override the defaults, e.g. I have:
/* override alert dialog defaults */
.react-confirm-alert-overlay {
background: rgba(0,0,0,0.5);
}
.react-confirm-alert {
background: white;
width: 80%;
padding: 1em;
}
/* custom alert dialog styles */
.add-dialog h3 {
margin: 0;
}
.add-dialog-buttons {
float: right;
}
.add-dialog-buttons button+button {
margin-left: 0.5em;
}
which looks like this -
You can use this npm package. https://github.com/gregthebusker/react-confirm-bootstrap.
Once you have installed it, you can use it like this in your project.
<ConfirmationModal
onConfirm={this.onConfirm}
body="Are you sure?"
confirmText="Yes"
cancelText="No"
title="Delete confirmation">
<Button>Button Text</Button>
</ConfirmationModal>
I have been using this package in my project with a few modifications. But the default package should be more than enough for your use case.
Best thing for custom modal designing is react-bootstrap
React-bootstrap contain its own Modal component, which is can be molded according to your own custom design, while having bootsrap helps you with other designing things in your application too.
Modal Component is easy to use,handle & implement. By default it have its own cancel/ok buttons in it, you just need to implement and use.
here is the link:
https://react-bootstrap.github.io/components/modal/
Hope that will help you.
Happy Coding!