I would like to have modals and notifications in my app and coming from using old jQuery Bootstrap, creating modals and notifications were really easy but now I am pretty confused on how to implement this in the virtual DOM using the react component system.
This is what I believe the standard react way to build modals in React within a component:
Index/Router Component >
Main Layout Component >
{...Page Components... }
{...Child Component}
{<Modal /> or <Notification />}
The issue with this is I dont want to constantly have to import and create a <Modal> or <Notification /> component within my sub components, instead maybe just call a utility function such as {app.notify({type: 'success', message: 'some message'})} or app.modal({...customconfig}) and have both defined within my Main layout component which get triggerd through any child components.
Any help on this would be great, thanks!
You do not need to keep your Modal component in a hierarchy. Your Modal component should be an independent component which would take appropriate props to decide what needs to be displayed. E.g.
<Modal message={"This is my modal"} showOkCancel={true} showYesNo={false} handleOkYes={()=>console.log("OK clicked")} handleCancelNo={()=>console.log("Cancel clicked"} />
In the above example, the Modal accepts a number of props which would help it decide the message to display, the buttons to display and the actions that need to take on said button click.
This kind of a component can reside outside your component hierarchy and can be imported into any component that needs to show a modal. The parent component would just need to pass the appropriate props to show the modal.
Hope this helps.
So here is the approach I took to resolve this.
First here is how you want to structure the modal and notification components:
{Index/Router Component}
{Main Layout Component <Modal /> or <Notification />}
{...Page Components... }
{...Child Component calls app.modal({...config}) or app.notify(...config)}
For notifications, I used a plugin called react-notification-system and for modal, I just wrote it myself.
Here is my code:
Layout.js
import React from "react";
import {Link} from 'react-router';
import NotificationSystem from 'react-notification-system';
import AppHeader from "#/ui/header/AppHeader";
import AppFooter from "#/ui/footer/AppFooter";
import Modal from "#/ui/modals/modal/Modal";
import "#/main.scss";
import './layout.scss';
export default class Layout extends React.Component {
constructor(props) {
super(props);
}
componentDidMount() {
app.notify.clear = this.refs.notificationSystem.clearNotifications;
app.notify = this.refs.notificationSystem.addNotification;
app.modal = this.refs.modal.updateProps;
}
render() {
return (
<div class="app">
<div class="header">
<AppHeader page={this.props.location.pathname.replace('/', '')}/>
</div>
<div class="body">
{this.props.children}
</div>
<div class="footer">
<AppFooter />
</div>
<NotificationSystem ref="notificationSystem" style={false} />
<Modal ref="modal" />
</div>
);
};
}
Modal.js
import React from "react";
import ReactDOM from 'react-dom';
import SVGInline from "react-svg-inline";
import {closeSvg} from '#/utils/Svg';
export default class Modal extends React.Component {
constructor(props) {
super(props);
this.state = {
showHeader: true,
showFooter: false,
title: "",
size: '',
className: '',
id: '',
footerContent: null,
showSubmitBtn: true,
showCancelBtn: true,
cancelBtnText: "Cancel",
successBtnText: "Save Changes",
onModalClose: () => {},
showModal: false,
html: () => {}
}
this.updateProps = this.updateProps.bind(this);
this.hideModal = this.hideModal.bind(this);
}
componentWillMount() {
var self = this;
var $modal = $(ReactDOM.findDOMNode(this));
}
componentDidUpdate(prevProps, prevState) {
if(this.state.showModal) {
$('body').addClass('modal-open');
} else {
$('body').removeClass('modal-open');
}
}
componentWillUnmount() {
// $('body').removeClass("modal-open");
}
componentWillReceiveProps(nextProps) {
console.log(nextProps);
}
updateProps(args) {
let merged = {...this.state, ...args};
this.setState(merged);
}
hideModal() {
this.setState({
showModal: false
});
this.state.onModalClose();
}
buildFooter() {
if(this.props.footerContent) {
return (
<div class="content">
{this.props.footerContent}
</div>
)
} else if(this.props.showCancelBtn && this.props.showSubmitBtn) {
return (
<div class="buttons">
<button type="button" class="btn btn-default" data-dismiss="modal" onClick={this.props.onModalClose}>{this.props.cancelBtnText}</button>
<button type="button" class="btn btn-success">{this.props.successBtnText}</button>
</div>
);
} else if(this.props.showCancelBtn) {
return (<button type="button" class="btn btn-default" data-dismiss="modal" onClick={this.props.onModalClose}>Close</button>);
} else if(this.props.showSubmitBtn) {
return (<button type="button" class="btn btn-success">Save changes</button>);
}
}
render() {
let {
id,
className,
onModalClose,
size,
showHeader,
title,
children,
showFooter,
showModal,
html
} = this.state;
return (
<div class={`modal-wrapper`} >
{
showModal ?
<div class={`modal fade in ${className}`} role="dialog">
<div class="bg" ></div>
<div class={`modal-dialog ${size}`}>
<div class="modal-content">
{ showHeader ?
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal">
<SVGInline svg={closeSvg} />
</button>
<h4 class="modal-title">{ title }</h4>
</div> : '' }
<div class="modal-body" >
{html()}
</div>
{ showFooter ?
<div class="modal-footer">
{ this.buildFooter() }
</div> : ''
}
</div>
</div>
</div>
: ''
}
</div>
);
}
}
then in any of your child components you can just call within your render function:
app.notify({
message: message,
level: 'error'
});
or
app.modal({
showModal: true,
className: "fullscreen-image-modal",
size: "modal-lg",
html: () => {
return (<img src={listingManager.LISTINGS_PATH + imgUrl} />);
}
})
Related
I'm trying to create a BaseOverlay component that basically teleports its content to a certain area of my application. It works just fine except there's an issue when using it with v-show... I think because my component's root is a Teleport that v-show won't work because Teleport is a template.
I figured I could then use inheritAttrs: false and v-bind="$attrs" on the inner content... this throws a warning from Vue saying Runtime directive used on component with non-element root node. The directives will not function as intended. It results in v-show not working on MyComponent, but v-if does work.
Any clues as to what I'm doing wrong?
Example
App.vue
<script setup>
import MyComponent from "./MyComponent.vue";
import {ref} from "vue";
const showOverlay = ref(false);
function onClickButton() {
showOverlay.value = !showOverlay.value;
}
</script>
<template>
<button #click="onClickButton">
Toggle Showing
</button>
<div id="overlays" />
<div>
Hello World
</div>
<MyComponent v-show="showOverlay" text="Doesn't work" />
<MyComponent v-if="showOverlay" text="Works" />
</template>
BaseOverlay.vue
<template>
<Teleport to="#overlays">
<div
class="overlay-container"
v-bind="$attrs"
>
<slot />
</div>
</Teleport>
</template>
<script>
export default {
name: "BaseOverlay",
inheritAttrs: false,
};
</script>
MyComponent.vue
<template>
<BaseOverlay>
{{text}}
</BaseOverlay>
</template>
<script>
import BaseOverlay from "./BaseOverlay.vue";
export default {
name: "MyComponent",
components: {
BaseOverlay
},
props: {
text: {
type: String,
default: ""
}
}
}
</script>
I would consider moving the modal/overlay dependency out of the component and into the app composition to make it more reusable.
Note the isMounted check check - this is to add a delay if the outlet containers have not yet been defined. You may need to add additional handling if your outlets are not pressent on mount e.g. <div id="outlet" v-if="false">
const {
createApp,
defineComponent,
ref,
onMounted
} = Vue
//
// Modal
//
const MyModal = defineComponent({
name: 'MyModal',
props: {
to: {
type: String,
required: true
},
show: Boolean
},
setup(){
const isMounted = ref(false);
onMounted(()=> isMounted.value = true )
return { isMounted }
},
template: `
<teleport :to="to" v-if="isMounted">
<div class="overlay-container" v-show="show">
<slot />
</div>
</teleport>
`
})
//
// Component
//
const MyComp = defineComponent({
name: 'MyComp',
template: `<div>Component</div>`
})
//
// App
//
const MyApp = defineComponent({
name: 'MyApp',
components: {
MyModal,
MyComp
},
setup() {
const modalShow = ref(false);
const modalOutlet = ref('#outletOne');
const toggleModalShow = () => {
modalShow.value = !modalShow.value
}
const toggleModalOutlet = () => {
modalOutlet.value = modalOutlet.value == '#outletOne'
? '#outletTwo'
: '#outletOne'
}
return {
toggleModalShow,
modalShow,
toggleModalOutlet,
modalOutlet,
}
},
template: `
<div>
<button #click="toggleModalShow">{{ modalShow ? 'Hide' : 'Show' }} Modal</button>
<button #click="toggleModalOutlet">Toggle Modal Outlet {{ modalOutlet }} </button>
</div>
<MyModal :to="modalOutlet" :show="modalShow">
<MyComp />
</MyModal>
<div id="outletOne">
<h2>Outlet One</h2>
<!-- outlet one -->
</div>
<div id="outletTwo">
<h2>Outlet Two</h2>
<!-- outlet two -->
</div>
`
})
//
// Assemble
//
const app = createApp(MyApp)
app.mount('body')
/* just some styling */
#outletOne { color: tomato; }
#outletTwo { color: olive; }
h2 { margin: 0; }
[id*=outlet]{ display: inline-flex; flex-direction: column; padding: 1rem; }
button { margin: 1rem; }
<script src="https://unpkg.com/vue#3.1.4/dist/vue.global.js"></script>
Wanted to follow-up on this. I started running into a lot of other issues with Teleport, like the inability to use it as a root element in a component (like a Dialog component because I want to teleport all dialogs to a certain area of the application), and some other strange issues with KeepAlive.
I ended up rolling my own WebComponent and using that instead. I have an OverlayManager WebComponent that is used within the BaseOverlay component, and every time a BaseOverlay is mounted, it adds itself to the OverlayManager.
Example
OverlayManager.js
export class OverlayManager extends HTMLElement {
constructor() {
super();
this.classList.add("absolute", "top-100", "left-0")
document.body.appendChild(this);
}
add(element) {
this.appendChild(element);
}
remove(element) {
this.removeChild(element);
}
}
customElements.define("overlay-manager", OverlayManager);
BaseOverlay.vue
<template>
<div class="overlay-container" ref="rootEl">
<slot />
</div>
</template>
<script>
import {ref, onMounted, onBeforeUnmount, inject} from "vue";
export default {
name: "BaseOverlay",
setup() {
const rootEl = ref(null);
const OverlayManager = inject("OverlayManager");
onMounted(() => {
OverlayManager.add(rootEl.value);
});
onBeforeUnmount(() => {
OverlayManager.remove(rootEl.value);
});
return {
rootEl
}
}
};
</script>
App.vue
<script setup>
import {OverlayManager} from "./OverlayManager.js";
import MyComponent from "./MyComponent.vue";
import {ref, provide} from "vue";
const manager = new OverlayManager();
provide("OverlayManager", manager);
const showOverlay = ref(false);
function onClickButton() {
showOverlay.value = !showOverlay.value;
}
</script>
<template>
<button #click="onClickButton">
Toggle Showing
</button>
<div>
Hello World
</div>
<MyComponent v-show="showOverlay" text="Now Works" />
<MyComponent v-if="showOverlay" text="Works" />
</template>
<style>
.absolute {
position: absolute;
}
.top-100 {
top: 100px;
}
.left-0 {
left: 0;
}
</style>
This behaves exactly how I need it, and I don't have to deal with the quirks that Teleport introduces, and it allows me to have a singleton that is in charge of all of my overlays. The other benefit is that I have access to the parent of where BaseOverlay is initially added in the HTML (not where it's moved). Honestly not sure if this is a good practice, but I'm chuffed at how cute this is and how well Vue integrates with it.
Thanks for taking the time to read this. I have been at this now for a few hours and nearly got it right except now, Im Getting a "TypeError: render is not a function" Error and Can not figure it out, using Context API and Reactjs Please help
I have changed around States and props and back again this is the closest I got it to work
this is my parent file where I pass the UserProvider
import React, { Component } from "react";
import Group from "./Groups";
import JsonData from "../data/export";
import { UserProvider } from "./context/UserContext";
class GroupGrid extends Component {
render() {
// console.log(JsonData);
return (
<UserProvider value={JsonData}>
<div className='cards'>
<Group />
</div>
</UserProvider>
);
}
}
export default GroupGrid;
this is the child file
import React, { Component } from "react";
import UserContext from "../components/context/UserContext";
import { UserConsumer } from "../components/context/UserContext";
class GroupDetails extends Component {
static contextType = UserContext;
componentDidMount() {
const data = this.context;
// console.log(data); // { name: 'Tania', loggedIn: true }
}
render() {
return (
<UserConsumer>
{this.context.TransportGrid.GROUPS.map((value, index) => (
<div key={value.GROUP_ID} className='GroupDetail'>
<div className='groupRow1'>
<span className='Date_label'>Date:</span>
<span className='GroupDate'>
{value.GROUP_DATE}
</span>
<span className='Group_label'>Group No:</span>
<span className='GroupID'>{value.GROUP_ID}</span>
</div>
<div className='groupRow2'>
<span className='Driver_label'>Driver:</span>
<span className='DriverName'>
{value.DRIVER_NAME}
</span>
<span className='Reg_label'>Artic:</span>
<span className='VehcileReg'>
{value.VEHICLE_REG}
</span>
</div>
</div>
))}
</UserConsumer>
);
}
}
export default GroupDetails;
and this an example of the JSON it looking at
{
"TransportGrid": {
"GROUPS": [
{
"GROUP_ID": "1234",
"GROUP_DATE": "20/08/2019",
"DRIVER_ID": "22",
"DRIVER_NAME": "JIMMY BLOGGS",
"VEHICLE_REG": "XSRFDFDDF",
"START_COUNTRY": "COUNTRY1",
"END_COUNTRY": "COUNRTY2",
"MOVEMENTS": [
{
this is repeated like 180 times ANyone got any idea's?
Thanks in Advance
Problem is that your consumer from context API needs to just have a single children element as a function whereas you are having an array of JSX elements as children.
Also since you are using the latest context API you don't need to use the Consumer component, you could simply write your code as
class GroupDetails extends Component {
static contextType = UserContext;
componentDidMount() {
const data = this.context;
// console.log(data); // { name: 'Tania', loggedIn: true }
}
render() {
return (
<React.Fragment>
{this.context.TransportGrid.GROUPS.map((value, index) => (
<div key={value.GROUP_ID} className='GroupDetail'>
<div className='groupRow1'>
<span className='Date_label'>Date:</span>
<span className='GroupDate'>
{value.GROUP_DATE}
</span>
<span className='Group_label'>Group No:</span>
<span className='GroupID'>{value.GROUP_ID}</span>
</div>
<div className='groupRow2'>
<span className='Driver_label'>Driver:</span>
<span className='DriverName'>
{value.DRIVER_NAME}
</span>
<span className='Reg_label'>Artic:</span>
<span className='VehcileReg'>
{value.VEHICLE_REG}
</span>
</div>
</div>
))}
</React.Fragment>
);
}
}
export default GroupDetails;
I'm having an issue with editing a value in my React app. I'm aware of how controlled components work, and my problem isn't related to that.
I can paste text into it and see state for the input change, but when I try to change it myself nothing happens. The input resides inside of a TableHeader component.
import React from "react";
import classNames from "classnames";
class TableHeader extends React.Component {
constructor(props) {
super(props);
this.state = {
columnFilterText : "",
filterBoxOpen : false
}
this.toggleSortBox = this.toggleSortBox.bind(this);
this.handleColumnInputChange = this.handleColumnInputChange.bind(this);
}
toggleSortBox(event, value) {
if(event.target === event.currentTarget) {
this.setState({
filterBoxOpen: !this.state.filterBoxOpen
});
}
}
handleColumnInputChange(event) {
console.log(event)
this.setState({
columnFilterText: event.target.value
})
}
render() {
let tableHeaderClasses = classNames({
"sortable" : true,
"filter-box-open" : this.state.filterBoxOpen
});
let sortOptionClasses = classNames({
"sort-option" : true
});
return (
<th className={tableHeaderClasses} onClick={this.toggleSortBox}>
<div className="sort-box">
<div className="sort-option-container">
<div className={sortOptionClasses}>Sort - ascending</div>
</div>
<div className="sort-option-container">
<div className={sortOptionClasses}>Sort - descending</div>
</div>
<hr className="divider" />
<input onChange={this.handleColumnInputChange} type="text" value={this.state.columnFilterText} />
<div className="row">
<button className="six columns">Apply</button>
<button className="six columns">Clear</button>
</div>
</div>
{this.props.label}
</th>
);
}
}
export default TableHeader;
I've checked if it's a css issue, by disabling styles, but the input field is still disabled, so no luck there. Any ideas what might be causing the issue?
I'm an idiot. The reason for this was that I had a keyDown event on the parent component for keyboard navigation, with event.preventDefault(); which of course affected the child component.
Thanks to everyone for contributing and for trying to help!
First of all I am new in React. I have two components TagUtils and Urls. I am trying to pass router paramter from Urls to TagUtils. Here is my codes:
Urls.jsx
// some codes....
export default class Urls extends React.Component {
render() {
return (
<div>
<TagUtils tag={this.props.params.tag_id}/>
</div>
)
}
}
TagUtils.jsx
export default class TagUtils extends React.Component {
deleteTag(props) {
console.log(props.tag);
}
render() {
return (
<div className="col-xs-6 col-md-4">
<button type="button" className="btn btn-danger" onClick={this.deleteTag}><i className="fa fa-trash"> Delete</i></button>
</div>
)
}
}
When I clicked Delete button it just showing undefined .Maybe I am missing something.
In your example props is event Object where there is no tag property - that's why you are getting undefined., you need set this for deleteTag and then you can get component props through this.props inside deleteTag method
export default class TagUtils extends React.Component {
constructor() {
this.deleteTag = this.deleteTag.bind(this);
}
deleteTag() {
console.log(this.props.tag);
}
render() {
return (
<div className="col-xs-6 col-md-4">
<button type="button" className="btn btn-danger" onClick={this.deleteTag}>
<i className="fa fa-trash"> Delete</i>
</button>
</div>
)
}
}
With the shift of React from createClass to ES6 classes we need to handle the correct value of this to our methods on our own, as mentioned here: http://www.newmediacampaigns.com/blog/refactoring-react-components-to-es6-classes
Change your code to have the method bounded to correct value of this in constructor:
export default class TagUtils extends React.Component {
constructor(props) {
super(props);
this.deleteTag = this.deleteTag.bind(this);
}
deleteTag(props) {
console.log(props.tag);
}
render() {
return (
<div className="col-xs-6 col-md-4">
<button type="button" className="btn btn-danger" onClick={this.deleteTag}><i className="fa fa-trash"> Delete</i></button>
</div>
)
}
}
The no autobinding was a deliberate step from React guys for ES6 classes. Autobinding to correct context was provided with React.createClass. Details of this can be found here: https://facebook.github.io/react/blog/2015/01/27/react-v0.13.0-beta-1.html#autobinding
So based on this you could also change your code as:
export default class TagUtils extends React.Component {
deleteTag = (props) => {
console.log(props.tag);
}
render() {
return (
<div className="col-xs-6 col-md-4">
<button type="button" className="btn btn-danger" onClick={this.deleteTag}><i className="fa fa-trash"> Delete</i></button>
</div>
)
}
}
I have an ecosystem of 4 inter-related components and it'd be very helpful if I can manipulate one component's state from a different one.
This is my MidSection component:
import React, {Component} from 'react';
import {render} from 'react-dom';
import $ from 'jquery';
import List from './List';
import OpenSessionCard from './OpenSessionCard';
class MidSection extends Component {
constructor() {
super(...arguments);
this.state = {
cardsToBeDisplayed: this.props.sessionCards,
cardsFilter: 'All',
cardExpanded: false,
cardToBeDisplayed: null
};
}
filterCards() {
let selectedValue = $('#cards-filter').val();
if (selectedValue === 'All') {
this.setState({
cardsToBeDisplayed: this.props.sessionCards,
cardsFilter: 'All',
cardExpanded: false,
cardToBeDisplayed: null
});
} else {
this.setState({
cardsToBeDisplayed: this.props.sessionCards.filter((sessionCard) => sessionCard.status === selectedValue),
cardsFilter: selectedValue,
cardExpanded: false,
cardToBeDisplayed: null
});
}
}
render() {
let cardList, openSessionCard;
if (!this.state.cardToBeDisplayed) {
cardList = (
<List cards={this.state.cardsToBeDisplayed} filter={this.state.cardsFilter}/>
);
} else {
openSessionCard = (
<OpenSessionCard card={this.state.cardToBeDisplayed}/>
);
};
return (
<section className="col-md-8 col-sm-12 col-xs-12 middle-section-container">
<div className="nav-justified pull-left">
<div className="gray-background-color col-xs-12 form-control-static">
<div className="col-xs-6">
<label className="control-label green-color" htmlFor="inputError1">MY SESSIONS</label>
</div>
<div className="col-xs-2 col-xs-offset-4 text-right">
<select id="cards-filter" className="form-control" onChange={this.filterCards.bind(this)}>
<option>All</option>
<option>Open</option>
<option>Scheduled</option>
<option>Completed</option>
<option>Closed</option>
</select>
</div>
</div>
{cardList}
{openSessionCard}
</div>
</section>
);
}
componentDidMount() {
$('#mid-section').attr('data-rendered', 'true');
}
}
export default MidSection;
And this is my SessionCard component:
import React, {Component} from 'react';
import {render} from 'react-dom';
import $ from 'jquery';
import MidSection from './MidSection';
class SessionCard extends Component {
openCard() {
/*************
MidSection.setState({
cardsToBeDisplayed: null,
cardsFilter: null,
cardExpanded: true,
cardToBeDisplayed: this
});
**************/
$('#cards-filter').attr('disabled', 'disabled');
}
render() {
return (
<div className="card" onClick={this.openCard.bind(this)}>
<div className="card__title green-color">{this.props.name}</div>
<div className="card__details">
<span>Facilitator: {this.props.facilitator}</span><br/>
<span>Mode: {this.props.mode}</span><br/>
<span>Status: {this.props.status}</span><br/>
</div>
</div>
);
}
}
export default SessionCard;
I want the openCard() function in the SessionCard component to call the setState() function of the MidSection component. Is there any way I can achieve this? How do I refer to the MidSection component (with its current state) from the SessionCard component?
You should only set state from the main (parent) component. All children components should be "dumb" components. Pass in the function you are wanting to call as a prop of SessionCard like :
<OpenSessionCard card={this.state.cardToBeDisplayed} setStateFunc={this.setStateFunc}/>
And then in your openCard() function call :
this.props.setStateFunc();
This will call the function in the parent component and allow you to manipulate the state from there.
Thanks a lot #erichardson30! Even though your solution didn't totally suffice, yet it guided me to the right track. Passing the setState() function as a prop only passed the function, without the context (read: the component on which it is supposed to be called). So, I passed a reference to the MidSection component itself as a prop, and then in the openCard() function, I invoked the setState() function of the component in its props.
In my MidSection component:
<OpenSessionCard card={this.state.cardToBeDisplayed} midSectionComponent={this}/>
And in my openCard() function:
let midSectionComponent = this.props.midSectionComponent;
midSectionComponent.setState({
cardsToBeDisplayed: null,
cardsFilter: null,
cardExpanded: true,
cardToBeDisplayed: this
});
It's working exactly the way I wanted it to. Thanks again! :)