I have a parent component which contains a child, which in turn contains a md-dialog component. I open the dialog from the component with this:
ParentComponent.vue
<template>
<div>
<ConfirmDialog
:dialogState="isDialogShown"
#closeDialog="value => isDialogShown = false"
/>
<button #click="handleOpenModal()">open modal</button>
</div>
</template>
<script>
export default {
name: 'ParentComponent',
data() {
return { isDialogShown: false }
},
methods: {
handleOpenModal() {
this.isDialogShown = true;
}
}
}
</script>
Button click opens this dialog:
ConfirmDialog.vue
<template>
<md-dialog :md-active.sync="localDialogState">
markup
</md-dialog>
</template>
<script>
export default {
name: "ConfirmDialog",
props: ["dialogState"],
computed: {
localDialogState: {
get() {
return this.$props.dialogState;
},
set() {
this.handleCloseDialog();
}
}
},
methods: {
handleCloseDialog() {
this.$emit("closeDialog");
}
}
};
</script>
I don't really like this because it uses this localDialogState's setter to perform this side effect which only eventually funnels back down to set localDialogState after setting the prop on the parent, however so far I've had to use the setter so I can capture events like backdrop click or ESC keypress that are meant to close the modal.
NB, I have had to use this "local" version of dialog state because apparently props and computed props are all just kept on this object's instance, so the names collide otherwise. There might also be a better way to do this which I am open to hear suggestions on since the above are my first lines of Vue I have ever written.
What is the canonical way of updating these (.sync'd) props in order to catch close events triggered by md-mialog?
Obviously a dialog cannot be used by only one boolean prop. That way you can't distinguish when a dialog was cancelled and when it was confirmed. In more complex cases you may have several confirm buttons with different actions.
I recommend to use a method like open to open a dialog by using $refs in a parent component and close a dialog inside itself with emitting confirmation events that can be catched in a parent component.
Related
I have a Modal component, which uses Bulma CSS' modal:
<script>
import { createEventDispatcher } from 'svelte';
export let active;
export let closeable = true;
const dispatch = createEventDispatcher();
const closeModal = () => {
active = false;
dispatch("closeModal");
};
const closeModalSoft = () => {
if (closeable) {
closeModal();
}
};
const closeModalKeyboard = (event) => {
if (event.key === "Escape" && closeable) {
closeModal();
}
};
</script>
<svelte:window on:keydown={closeModalKeyboard}/>
<div class="modal is-clipped" class:is-active={active}>
<div class="modal-background" on:click={closeModalSoft}/>
<div class="modal-content">
<div class="container">
<slot />
</div>
</div>
{#if closeable}
<button class="is-large modal-close" aria-label="close" on:click={closeModal}/>
{/if}
</div>
It should allow for arbitrary nesting, so you can for example have a modal over a modal over the rest of the website.
I would like to allow for modals be closed by pressing the close button, clicking outside of the modal or using the escape key. I would like this to operate like a stack: the topmost modal gets closed first. (Note: If a modal is not closeable as shown in my code, it just means that the modal can only be closed by manipulating active externally).
Currently, the close button and clicking outside the modal work with nested modals. However, escape will always close all modals, instead of just the topmost one. But, given the code, I think this is to be expected.
What would I need to change such that only the topmost (closeable=true) modal gets closed?
I have thought about the following approaches, but I feel like there must be better ways:
On escape, determine the element at the centre of the screen, and only if its ID is equal to some ID I will give each modal, close it.
On escape, query the DOM element and see if it has any children/siblings after itself that have both the modal and is-active classes. If so, ignore the keypress.
Perhaps use :focus or other modifiers on the topmost element and then a similar approach as the one above.
Adding the event on the window is the wrong approach which leads to this issue.
Modals should not allow focus to leave it, that being the case you should be able to handle the Escape press on the Modal itself. The easiest way to get the focus trap "for free" would be to use a native dialog element (though it getting support is still relatively recent).
A manual focus trap would have to steer focus on opening/closing and on any Tab press. There might be libraries that already implement this. Actually, I would not recommend implementing such generic components anyway and suggest the use of a component library instead. It is easy to get accessibility and things like keyboard interactions very wrong.
Guidelines for Modals: https://www.w3.org/WAI/ARIA/apg/patterns/dialogmodal/
I am using the TabView component from PrimeVue (Vue 3), and I want to stop the tab change if any changes are made by the user, the problem is that I don't know how. I've already tried passing the event and using preventDefault and stopPropagation but seems that it doesn't work and click event is still happening.
The procedure should be:
If any changes are made, user press the tab and a dialog appears.
If user clicks 'No', I should prevent the tab change and stop the click event
Here is the demo of what I'm trying to archive, should be simple https://codesandbox.io/s/aged-wave-yzl1k?file=/src/App.vue:0-1753
If a flag is true I want to show a confirm dialog and prevent the tab change if user dismiss it.
The component that I'm using for the TabView: https://primefaces.org/primevue/showcase/#/tabview
Thanks in advance,
From the docs it looks like that internally the component will first switch tabs and then emit "tab-click", which explains the issue you're seeing. The exception is if the tab is disabled, in which case it won't change tabs but will emit "tab-click".
It took a bit to figure out, but there is a way to get the functionality you need with only a small adjustment. It requires a change in your main.js as well as in your App.vue file.
// main.js
/*
* Put this after you import TabView.
* This will prevent automatic tab switching but still emits
* the event to your application.
*/
TabView.methods.onTabClick = function(event, i) {
this.$emit('tab-click', {
originalEvent: event,
index: i
});
}
// App.vue
const onTabClick = (event) => {
if (changes.value) {
confirm.require({
message:
"Are you sure that you want to leave this tab? You'll lose your changes",
icon: "fal fa-exclamation-triangle",
acceptLabel: "Yes",
rejectIcon: "No",
accept: () => {
alert("here we should allow tab change");
activeIndex.value = event.index; // manually set activeIndex
},
reject: () => {
alert("stop tab change");
},
});
}
};
These changes modify what the onTabClick library method to only emit the event, without automatically switching. Then in your app you can check the index property of the event to determine what should be set to active.
I'm struggling with getting a modal to appear onClick(). I have a function within a component that adds players to an existing list when clicking on Add Player. The button is rendered separately in a renderAddButton() function, which passes onAddButtonClick() as a prop.
I would like for the user to be able to input the player's name in a form within a modal before it is added to the list, right now the code outputs a Player + index as the name of the player.
function onAddButtonClick() {
setItems((prev) => {
const newItems = [...prev];
newItems.push({
name: `Player ${newItems.length + 1}`,
teamId: currentTeam[0].teamId
});
playersStore.push({
name: `Player ${newItems.length + 1}`,
teamId: currentTeam[0].teamId
});
return newItems;
});
}
I have this form which I want to represent in the modal:
export const PlayerForm = () => {
return (
<div>
<form>
<input type='string' id='playerId' name='playerName' defaultValue='0' />
<input
type='number'
id='playerGoals'
name='totalGoals'
defaultValue='0'
min='1'
max='5'
/>
<input
type='number'
id='playerGoals'
name='playerGoalPercentage'
defaultValue='0'
min='1'
max='5'
/>
</form>
</div>
);
};
How do I trigger the modal from inside onAddButtonClick()?
I implement modals using the react-bootstrap framework.
From the component that I want to display the modal from, I will create a handler that will govern the component's ability to show the modal based on the bool I set in state. Typically from the parent component this show handler would look like this:
setShow = () => {
this.setState({ show: !this.state.show });
};
As seen in the example this handles a state attribute called show which is what dictates whether or not the modal gets to display in app.
Below is the implementation of the modal I would use as a child component to the parent component where it would reside and where I would pass the state attribute which I called show that dictates with true or false whether or not to display the modal:
<ExampleModal
show={this.state.show}
setShow={this.setShow}
activeRecord={this.state.activeRecord}
activePrimaryAccountId={this.state.activePrimaryAccountId}
userAccessRole={this.props.userAccessRole}
/>
I pass the necessary details that the modal needs to display as props that I get from the the parent component's state attributes. The most important being the show attributes to include the setShow function which I use in the child component (the modal itself) to update state in the parent component to close the modal when the time comes also.
In the ExampleModal component I start off with declaring state with the following attributes already loaded from props:
this.state = {
show: this.props.show,
...
}
I then use a handler that takes advantage of the setShow function passed down to the child component in props as shown:
handleClose = () => this.props.setShow(false);
In the modal component there is a button that uses this handler in its onClick() synthetic event to trigger the closing of the modal after it has rendered to the browser.
Conversely in the parent component, your button will use the onClick() synthetic event to trigger a call that would be implemented something like this in the button to open the modal:
onClick={this.setShow(true)}
I reuse that process in all of my modals in React.js, hope that helps. The trick here is using componentDidUpdate() or useEffect() (if you're using React Hooks) effectively to make sure you have the right data loaded in state in the parent component so that you can pass it into the props of the child component at the right time. The <ExampleModal /> I gave you should give you enough of a clue.
Background:
I have a component inside a template. This component is a pop up box which opens on a button click. This pop up box has check boxes (all by default set to false) inside of it. When I close this pop up box I want to completely reset all of the variables to the default settings, i.e. all checkboxes are now turned off. Currently, when I re-open this popup box the previous checkboxes are still checked. How do I do this without manually toggling every checkbox:
this.set("checkbox1", false);
this.set("checkbox2", false);
so on...
Is there a function that will automatically reset the component and uncheck all the checkboxes and set the variables back to false?
Relevant Code:
Template: app/template/home.hbs
{{popup-modal isOpen=showModal}}
Component: app/template/components/popup-modal.hbs
{{#bs-modal body=false footer=false open=isOpen title="popup box" closeAction=(action "cancel") submitAction=(action "submit")}}
{{#bs-modal-body}}
<label><input type="checkbox" {{action "toggleCheckbox" "checkbox1" on="click" preventDefault=false}}/> Checkbox 1</label>
<label><input type="checkbox" {{action "toggleCheckbox" "checkbox2" on="click" preventDefault=false}}/> Checkbox 2</label>
{{/bs-modal-body}}
{{bs-modal-footer closeTitle="Cancel" submitTitle="Ok"}}
{{/bs-modal}}
Component JS: app/components/popup-modal.js
import Ember from 'ember';
export default Ember.Component.extend({
checkbox1: false,
checkbox2: false,
actions: {
submit(){
// close box
this.set('isOpen', false);
},
cancel(){
// how do I reset component here?
// in other words, make all checkbox set to false
// without manually doing it like below:
this.set("checkbox1", false);
this.set("checkbox2", false);
},
toggleCheckbox(checkbox){
this.toggleProperty(checkbox);
}
}
});
Many times i've also had similar situation. And sometimes the values were not just boolean but strings or numeric (non-zero) values, so I also needed to be able to reset some properties of the component to initial state.
In my opinion resetting all properties of the component is not so good (i don't know if it's even possible), because somebody would like to add some property which should keeps it's state even after user clicks "Cancel" button.
I think good idea is to make some function which will set every properties to initial state. For example your code can looks like:
import Ember from 'ember';
export default Ember.Component.extend({
// EmberJS Component hook fired after component have been initialized
init() {
this._super(...arguments);
this.setInitialState();
},
setInitialState(){
this.set("checkbox1", false);
this.set("checkbox2", false);
},
actions: {
submit(){
// close box
this.set('isOpen', false);
this.setInitialState();
},
cancel(){
this.setInitialState();
},
toggleCheckbox(checkbox){
this.toggleProperty(checkbox);
}
}
});
PS. This is good that components lives and changes it's state (for example properties) without need to be initialized again if it weren't destroyed. Because when some properties are changed not the whole component is rerendered, but only things which were changed (in this case just checkboxes).
As others have said, it would be useful to have a reproduction of your code.
However - the answer is likely going to be setting the defaults you want (false in your case) in one of the component lifecycle hooks.
init is probably fine, assuming that the component gets destroyed when you close it from the template.
I am trying to transfer data from native iOS app to a React (not react-native) web app running in a UIWebView.
I get a string from native viewcontroller and need to "inject" it into the value of an <input> element inside a React component class. I gave the element an id like so:
render() {
<div>
...
<input id="iccid" type="text" name="iccid" pattern="\d*" onChange={this.iccidChanged.bind(this)} autoFocus />
...
</div>
}
Then using JavaScriptCore from iOS (Swift code) I run:
self.context = self.webView.valueForKeyPath("documentView.webView.mainFrame.javaScriptContext") as? JSContext
self.context!.evaluateScript("document.getElementById('iccid').value='\(barcodeValue)';")
This seems work fine and I can see the value updated in the DOM inside the webview, problem is that my React onChange function (that validates the value and changes component state) does not fire as React does not interpret this as a change (probably because of DOM vs virtual DOM handling).
What's the correct way/best practice to update the element value from iOS AND get the React component to behave like user has typed it in?
I'm supposing that you want to trigger an event when a barcode is scanned. Your iOS app probably doesn't need to know where the event should be dispatched (i.e. which input field and how). It should be enough just to signal the web view that a barcode has been scanned.
You can dispatch a custom DOM event to window and have some React component listen to that event.
window.dispatchEvent(new CustomEvent('barcodescan', {
detail: {
value: '(barcodeValue)'
}
}))
You can then create a non-rendering component that will listen to the barcode event:
const BarcodeListener = React.createClass({
propTypes: {
onBarcodeScan: React.PropTypes.func.isRequired
},
componentDidMount () {
window.addEventListener('barcodescan', this.handleBarcodeScan)
},
componentWillUnmount () {
window.removeEventListener('barcodescan', this.handleBarcodeScan)
},
render () {
return null
},
handleBarcodeScan (e) {
this.props.onBarcodeScan(e)
}
})
Then, in your form, you can render the input alongside with the barcode event listener:
render () {
return (
<div>
...
<input ... />
<BarcodeListener onBarcodeScan={this.handleBarcodeScan} />
</div>
)
}
Then handle the barcode scan event separately.