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!
Related
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
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 get a vue component to announce information dynamically to a screen reader when different events occur on my site.
I have it working to where clicking a button will populate a span that is aria-live="assertive" and role="alert" with text. This works decently the first time, however, clicking other buttons with similar behavior causes NVDA to read the previous text twice before reading the new text. This seems to be happening in vue, but not with a similar setup using jquery, so I'm guessing it has something to do with the way vue renders to the DOM.
I'm hoping there is some way to workaround this problem or perhaps a better way to read the text to the user that would not have this issue. Any help is greatly appreciated.
Here is a simple component I set up in a working code sandbox to show the problem I am having (navigate to components/HelloWorld.vue for the code) -- Note: This sandbox has changed per the answer below. Full code for the component is below:
export default {
name: "HelloWorld",
data() {
return {
ariaText: ""
};
},
methods: {
button1() {
this.ariaText = "This is a bunch of cool text to read to screen readers.";
},
button2() {
this.ariaText = "This is more cool text to read to screen readers.";
},
button3() {
this.ariaText = "This text is not cool.";
}
}
};
<template>
<div>
<button #click="button1">1</button>
<button #click="button2">2</button>
<button #click="button3">3</button><br/>
<span role="alert" aria-live="assertive">{{ariaText}}</span>
</div>
</template>
Ok so what I've found works way more consistently is instead of replacing the text in the element with new text, to add a new element to a parent container with the new text to be read. Instead of storing the text as a single string, I am storing it in an array of strings which will v-for onto the page within an aria-live container.
I have built a full component that will do this in various ways as an example for anyone looking to do the same:
export default {
props: {
value: String,
ariaLive: {
type: String,
default: "assertive",
validator: value => {
return ['assertive', 'polite', 'off'].indexOf(value) !== -1;
}
}
},
data() {
return {
textToRead: []
}
},
methods: {
say(text) {
if(text) {
this.textToRead.push(text);
}
}
},
mounted() {
this.say(this.value);
},
watch: {
value(val) {
this.say(val);
}
}
}
.assistive-text {
position: absolute;
margin: -1px;
border: 0;
padding: 0;
width: 1px;
height: 1px;
overflow: hidden;
clip: rect(0 0 0 0);
}
<template>
<div class="assistive-text" :aria-live="ariaLive" aria-relevant="additions">
<slot></slot>
<div v-for="(text, index) in textToRead" :key="index">{{text}}</div>
</div>
</template>
This can be used by setting a variable on the parent to the v-model of the component, and any changes to that variable will be read to a screen reader once (as well as any time the parent container becomes tab-focused).
It can also be triggered by this.$refs.component.say(textToSay); -- note this will also be triggered again if the parent container becomes tab-focused. This behavior can be avoided by putting the element within a container that will not receive focus.
It also includes a slot so text can be added like this: <assistive-text>Text to speak</assistive-text> however, that should not be a dynamic/mustache variable or you will encounter the problem in the original question when the text changes.
I've also updated the sandbox posted in the question with a working example of this component.
I've been messing around with aurelia-dialog trying to get a modal dynamically populated with some information. I have some stuff working but the modal is the incorrect size for the data its displaying.
welcome.js
import {DialogService} from 'aurelia-dialog';
import {CmdModal} from './cmd-modal';
export class Welcome {
static inject = [DialogService];
constructor(dialogService) {
this.dialogService = dialogService;
}
OpenCmd(intName, opName, opDescription, parameters){
var cmd = { "CmdName" : opName, "Description" : opDescription, "Params" : parameters};
this.dialogService.open({ viewModel: CmdModal, model: cmd}).then(response => {
if (!response.wasCancelled) {
console.log('good - ', response.output);
} else {
console.log('bad');
}
console.log(response.output);
});
}
cmd-modal.html
<template>
<ai-dialog>
<ai-dialog-header>
<h2>${cmd.CmdName}</h2>
</ai-dialog-header>
<ai-dialog-body>
<p>${cmd.Description}</p>
<b>Parameters</b>
<div repeat.for="param of cmd.Params">
<p class="col-md-6">${param.Key}</p>
<p class="col-md-6">${param.Value}</p>
</div>
</ai-dialog-body>
<ai-dialog-footer>
<button click.trigger="controller.cancel()">Cancel</button>
<button click.trigger="controller.ok(person)">Ok</button>
</ai-dialog-footer>
</ai-dialog>
</template>
cmd-modal.js
import {DialogController} from 'aurelia-dialog';
export class CmdModal {
static inject = [DialogController];
constructor(controller){
this.controller = controller;
}
activate(cmd){
this.cmd = cmd;
}
}
When a link is clicked, a modal like the following is displayed:
As the image shows, the modal is the wrong size for the body and some of the text spills over the side. I think this is because cmd-modal.html is being rendered before the data for the repeater has been inserted.
Does anybody know how I could resize the modal to be the correct size for the body or delay the modal display until cmd-modal.htmlhas been correctly evaluated?
You can add style for width and height to the ai-dialog tag like this:
<ai-dialog style="width:600px; height: 350px;">
I think I found something similar to this when trying to add items of varying width to the dialog. The widths weren't know until after the dialog had been rendered. Well I think that is why!
In the end I added a CSS class on the ai-dialog element which included a general width setting and a media query.
...
width: 90vw;
#media (min-width: 46em) {
width: 44em;
}
....
I know I mixed vw and em measurements and there's probably better ways - but it works well in this app. I'm sure there's probably a "correct" Aurelia way to get the dialog to re-render but this is ample for our situation.
FWIW I also added a "margin-top: 4em !important" so that the dialog would appear just below the fixed header bar that Bootstrap was providing us.