I am making a file manager app based on react-redux, and I meet problem with input.
For example, my code:
PathForm.js:
export default class PathForm extends Component {
render() {
const { currentPath, handleSubmit } = this.props;
console.log('PathFormPathFormPathForm', this.props)
return (
<div className="path-box">
<form onSubmit={handleSubmit}>
<div>
<input type="text" className="current-path-input" placeholder="input path" value={currentPath} />
</div>
<button className="go-btn" type="submit">Go</button>
</form>
</div>
);
}
}
Explorer.js:
class Explorer extends Component {
goPath(e) {
e.preventDefault()
// fake function here, because I have to solve the input problem first
console.log('PathForm goPath:',this.props)
let {targetPath , actions} = this.props
swal(targetPath)
}
render() {
const { node, currentPath , actions} = this.props
console.log('Explorer.render:',this.props)
return (
<div className='explorer-container'>
<PathForm currentPath={currentPath} handleSubmit={this.goPath.bind(this)}/>
<FileListOperator />
<FileListView fileList={node && node.childNodes} actions ={actions} />
</div>
);
}
}
function mapStateToProps(state, ownProps) {
return {
node: state.tree[state.tree.currentPath],
currentPath: state.tree.currentPath
};
}
function mapDispatchToProps(dispatch) {
console.log('mapDispatchToProps')
return {
actions: bindActionCreators(NodeActions, dispatch)
};
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(Explorer);
Feature I want:
I have a PathForm, it need show path from two way:
user click a file path from left tree view, Explorer get this path as currentPath, then pass to PathForm, and show currentPath in input
user directly type a path to the PathForm's input, PathForm call handleSubmit(Explorer's function) to change the currentPath
Additional:I want to keep PathForm as a stateless component
The problem:
I'd like use PathForm as a stateless form, so I don't want connect it to store, but I need it change input by currentPath. But if I set value={currentPath}, user can not type anything else.
change to <input type="text" onChange={this.changeValue} value={this.getValue()}/> allow user type string in this input, but can not use props currentPath passed by Explorer
The only way I can imagine is connect this form to store which I don't want. I'd like Explorer to dispatch all actions and pass props.
Tried with some package
I found the input not act as my thought, so I tried the two popular package:
redux-form
It create a form need so much code, and official doc not say how to render this form with parent props,
I try to pass props and handleSubmit to it, not work. After I see
React + Redux - What's the best way to handle CRUD in a form component?
and How to wire up redux-form bindings to the form's inputs
I found I can't do that, it define some function overwrite mine, this behave is not good for me(I have to change the handlerSubmit function name, but it still not work), and it connect to the store. So I turn to formsy-react
formsy-react
It still need so much code, though it provide some mixin, but I still have to write a custom text input with changeValue function myself(changeValue is no need in most situation when writing normal html jquery app).Then I found the problem that PathForm can not use props currentPath passed by Explorer...
Probably Worked solution(but I don't tend to use):
connect PathForm to store, add another state inputPathValue for this input. Use inputPathValue interact with currentPath
After above, I found use input/form is super in-convenient in react....
Does it mean I have to connect PathForm to stroe?
Any other way to solve my problem?
There are uncontrolled(not set value) and controlled(set value) input in reactjs.
controlled not allow user input, but uncontrolled does.
Solution:
Need use uncontrolled input(no value attribute).
Select input element and set the value when currentPath change.
Bad way:
code:
export default class PathForm extends Component {
changeCurrentPath(path) {
const pathInput = document.querySelector('.current-path-input')
if (pathInput){
pathInput.value = path
this.lastPath = path
}
}
render() {
const { currentPath, handleSubmit } = this.props;
console.log('PathFormPathFormPathForm', this.props)
this.changeCurrentPath(currentPath)
return (
<div className="path-box">
<form onSubmit={handleSubmit}>
<div>
<input type="text" className="current-path-input" placeholder="input path" />
</div>
<button className="go-btn" type="submit">Go</button>
</form>
</div>
);
}
}
Good way:
use componentWillReceiveProps to set props and rel to select element
1.use form submit
export default class PathForm extends Component {
constructor(props) {
super(props)
// can not find `this` if not bind
this.handleSubmit = this.handleSubmit.bind(this)
}
componentWillReceiveProps(nextProps) {
if (nextProps.currentPath !== this.props.currentPath) {
this.setInputValue(nextProps.currentPath)
}
}
getInputValue() {
return this.refs.pathInput.value
}
setInputValue(val) {
this.refs.pathInput.value = val
}
handleSubmit(e){
e.preventDefault()
this.props.handleSubmit(this.getInputValue())
}
render() {
return (
<div className="path-box">
<form onSubmit={this.handleSubmit}>
<input className="current-path-input"
defaultValue={this.props.currentPath}
ref="pathInput" />
<button className="waves-effect waves-light btn" type="submit">Go</button>
</form>
</div>
);
}
}
2.use button click
export default class PathForm extends Component {
constructor(props) {
super(props)
// can not find `this` if not bind
this.handleGoClick = this.handleGoClick.bind(this)
this.handleKeyUp = this.handleKeyUp.bind(this)
}
componentWillReceiveProps(nextProps) {
if (nextProps.currentPath !== this.props.currentPath) {
this.setInputValue(nextProps.currentPath)
}
}
getInputValue() {
return this.refs.pathInput.value
}
setInputValue(val) {
this.refs.pathInput.value = val
}
handleKeyUp(e) {
if (e.keyCode === 13) {
this.handleGoClick()
}
}
handleGoClick(e) {
e.preventDefault()
this.props.handleSubmit(this.getInputValue())
}
render() {
return (
<div className="path-box">
<form >
<input className="current-path-input"
defaultValue={this.props.currentPath}
onKeyUp={this.handleKeyUp}
ref="pathInput" />
<button className="waves-effect waves-light btn" type="submit" onClick={this.handleGoClick}>Go</button>
</form>
</div>
);
}
}
If you really don't want the state in Redux, you can instead store the state on the component with setState. Directly accessing the input is strongly discouraged. You should track the state of the input on the component. Add an onChange handler to the input, store the state and handle componentWillReceiveProps where you decide what to do with new incoming props.
Related
as I'm on my Vue spree (started recently but so far I'm really enjoying learning this framework) couple of questions rised up. One of which is how to post form from multiple components. So before I continue forward I wanted to ask you what are you thinking about this way of structuring and point me in right direction if I'm wrong.
Here it goes.
I'm working on a SPA project using ASP.NET CORE 2.1 and Vue JS Template (with webpack)(https://github.com/MarkPieszak/aspnetcore-Vue-starter) and my project is structured in several containers, something like this:
In my app-root i registered several containers
<template>
<div id="app" class="container">
<app-first-container></app-first-container>
<app-second-container></app-second-container>
<!--<app-third-container></app-third-container>-->
<app-calculate-container></app-calculate-container>
<app-result-container></app-result-container>
</div>
</template>
<script>
// imported templates
import firstContainer from './first-container'
import secondContainer from './second-container'
import calculateContainer from './calculateButton-container'
//import thirdContainer from './third-container'
import resultContainer from './result-container'
export default {
components: {
'app-first-container': firstContainer,
'app-second-container': secondContainer,
// 'app-third-container': thirdContainer,
'app-calculate-container': calculateContainer,
'app-result-container': resultContainer
}
}
</script>
In my first container I'm having several dropdowns and two input fields with my script file where I'm fetching data from API and filling dropdowns and input fields with fetched data.
Something like this ( entered some dummy code for demonstration)
<template>
<div>
<h1>Crops table</h1>
<p>This component demonstrates fetching data from the server. {{dataMessage}}</p>
<div class="form-row">
<div class="form-group col-md-6">
<label for="exampleFormControlSelect1" class="col-form-label-sm font-weight-bold">1. Some text</label>
<select class="form-control" id="exampleFormControlSelect1" v-model="pickedCropType" #change="getCropsByType()">
<option v-for="(cropType, index) in cropTypes" :key="index" :value="cropType.id" :data-imagesrc="cropType.imgPath">{{ cropType.name }}</option>
</select>
</div>
<div class="form-group col-md-6">
<label for="exampleFormControlSelect2" class="col-form-label-sm font-weight-bold">2. Some text</label>
<select class="form-control" id="exampleFormControlSelect2">
<option v-for="(crop, index) in cropSelectList" :key="index" :value="crop.id">{{ crop.name }}</option>
</select>
</div>
</div>
</div>
</template>
<script>
import { mapActions, mapState } from 'vuex'
export default {
data() {
return {
cropTypes: null,
cropSelectList: null,
crops: null,
pickedCropType: null,
}
},
methods: {
loadPage: async function () {
try {
//Get crop types and create a new array with crop types with an added imgPath property
var cropTypesFinal = [];
let responseCropTypes = await this.$http.get(`http://localhost:8006/api/someData`);
responseCropTypes.data.data.forEach(function (element) {
cropTypesFinal.push(tmpType);
});
} catch (err) {
window.alert(err)
console.log(err)
}
},
getCropsByType: async function () {
//Get crops by crop type
let responseCrops = await this.$http.get(`http://localhost:8006/api/crop/Type/${this.pickedCropType}`);
var responseCropsData = responseCrops.data.data;
this.cropSelectList = responseCropsData;
}
},
async created() {
this.loadPage()
}
}
</script>
And in my second container I have different dropdowns and different input fields with different scripts etc.
So, my questions are:
1.) I'm having required data form field in first container and in second container I'm having additional data and my submit button is separated in third container (app-result-container). So, is this proper and logical way of structuring containers if not can you point me in right direction?
2.) Is it smart to input script tag in every container where I'm processing/fetching/submitting some data for that particular container? Should I put scripts tag in separated file and keep structure clean, separating html from js file.
Example:
import { something } from 'something'
export default {
data () {
return {
someData: 'Hello'
}
},
methods: {
consoleLogData: function (event) {
Console.log(this.someData)
}
}
}
3.) Can I send input values from one container to another (In my particular case from first and second container to app-calculate-container(third container))?
How to on submit return results container with calculated imported values
If you want components to communicate or share data with one another, you will need to either emit an event from one component up to the parent and pass it down via props, or use some kind of state management model, like Vuex, where each of your components can listen to the store.
Take a look at this code sandbox: https://codesandbox.io/s/8144oy7xy2
App.vue
<template>
<div id="app">
<child-input #input="updateName" />
<child-output :value="name" />
</div>
</template>
<script>
import ChildInput from "#/components/ChildInput.vue";
import ChildOutput from "#/components/ChildOutput.vue";
export default {
name: "App",
components: {
ChildInput,
ChildOutput
},
data() {
return {
name: ""
};
},
methods: {
updateName(e) {
this.name = e.target.value;
}
}
};
</script>
ChildInput.vue
<template>
<input type="text" #input="changeHandler">
</template>
<script>
export default {
name: "ChildInput",
methods: {
changeHandler(e) {
this.$emit("input", e);
}
}
};
</script>
ChildOutput.vue
<template>
<p>{{ value }}</p>
</template>
<script>
export default {
name: "ChildOutput",
props: {
value: {
type: String,
default: ""
}
}
};
</script>
What's going on?
The ChildInput component is a text field and on every change inside it, fires an event (emits using this.$emit() and passes the whole event up).
When this fires, App is listening to the change, which fires a method that updates the name data property.
Because name is a reactive data property and is being passed down as a prop to the ChildOutput component, the screen re-renders and is updated with the text written.
Neither ChildInput nor ChildOutput knows about one another. It's the parent that listens to the event passed to it, then passes the new prop down.
This way of working is fine and simple to understand, but I would strongly recommend looking at Vuex, as this method can get messy and complicated when you go beyond trivial tasks.
I'm using AureliaJS to build a dynamic forms scenario, where I have a parent form with the gross operations needed and multiple child's form's, that change based on user input.
These child's form's have only two specific things themselves. Their model and the validation rules for their model.
So my question is, how can the parent form call the validation rules from the current child form? From child I know that is possible call parent's view model. But from parent, how can I invoke any function from the child?
The scenario is similar off having one base class, that has one method and this method could be overriding on the child classes.
Any suggestion? I'm glad to change the approach if needed.
Here's an example: https://gist.run?id=1865041a15af60600cb7b538018bdccd
app.html
<template>
<span>This is an APP</span>
</p>
<compose view-model.bind="'parentForm'"></compose>
</template>
app.js
import { autoinject } from 'aurelia-framework';
#autoinject
export class App {
}
childForm1.html
<template>
<label> Price : </label>
<input value.bind="model.data.price">
<p/>
<label> VAT : </label>
<input value.bind="model.data.vat">
<p/>
</template>
childForm1.js
import { autoinject } from 'aurelia-framework';
#autoinject
export class ChildForm1 {
activate(model)
{
this.model = model;
}
validateRules (){
if(this.model.data.price != '' && this.model.data.vat == '' )
this.model.validateMessage = 'VAT is mandatory';
}
}
childForm2.html
<template>
<label>Address : </label>
<input value.bind="model.data.address">
<p/>
<label>Phone : </label>
<input value.bind="model.data.phone">
<p/>
</template>
childForm2.js
import { autoinject } from 'aurelia-framework';
#autoinject
export class ChildForm2 {
activate(model)
{
this.model = model;
}
validateRules (){
if(this.model.data.phone != '' && this.model.data.address == '' )
this.model.validateMessage = 'Address is mandatory';
}
}
index.html
<!doctype html>
<html>
<head>
<title>Aurelia</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body aurelia-app>
<h1>Loading...</h1>
<script src="https://jdanyow.github.io/rjs-bundle/node_modules/requirejs/require.js"></script>
<script src="https://jdanyow.github.io/rjs-bundle/config.js"></script>
<script src="https://jdanyow.github.io/rjs-bundle/bundles/aurelia.js"></script>
<script src="https://jdanyow.github.io/rjs-bundle/bundles/babel.js"></script>
<script>
require(['aurelia-bootstrapper']);
</script>
</body>
</html>
parentForm.html
<template>
<button click.delegate="changeChildForm1()">Change Child Form 1</button>
<button click.delegate="changeChildForm2()">Change Child Form 2</button>
<p/>
<p/>
<form>
<label>User : </label>
<input value.bind="model.data.user">
<p/>
<compose view-model.bind="childFormVM" model.bind="model"></compose>
<button click.delegate="save()">Save</button>
<p/>
<span> Validation Message : ${model.validateMessage}</span>
</form>
<p/>
<span>Price : ${model.data.price}</span><p/>
<span>Vat : ${model.data.vat}</span><p/>
<span>Phone : ${model.data.phone}</span><p/>
<span>Address : ${model.data.address}</span><p/>
</template>
parentForm.js
import { autoinject } from 'aurelia-framework';
#autoinject
export class ParentForm {
model = {
validateMessage : '',
data : {
user : 'My Name'
}
};
childFormVM = 'childForm1';
validateMessage = '';
changeChildForm1() {
this.childFormVM = 'childForm1';
}
changeChildForm2() {
this.childFormVM = 'childForm2';
}
save(){
this.validateRules();
// How to call the validation rules from child ?
}
validateRules (){
this.model.validateMessage = 'Validate by parent';
}
}
Bind a function call to the child so that you have a handle to invoke it from the parent. I usually prefer to directly bind the child components rather than using compose, but you can make it work with compose by passing a complex model object rather than only the model, and passing the binding function as one of the model properties.
Parent View-Model:
class Parent {
model = {};
child1Validate = null;
changeChildForm1() {
if (typeof this.child1Validate === 'function') {
// the binding was successful; proceed with function call
let result = this.child1Validate();
console.log(result);
}
}
}
Parent View:
<my-child1 model="parentModel" go-validate="child1Validate"></my-child1>
Child View-Model:
class MyChild1 {
#bindable model;
#bindable goValidate;
bind() {
// bind the child function to the parent that instantiates the child
this.goValidate = this.runValidation.bind(this);
}
runValidation() {
// do the validation and pass result to parent...
return 'Success!';
}
}
That's how you can do it:
parent-form.html
<compose view-model.bind="childFormVM" view-model.ref="childFormInstance" model.bind="model"></compose>
parent-form.js
save() {
this.childFormInstance.currentViewModel.validateRules();
}
Helpful Notes
Only use <compose> when necessary. For example, in the app.html you should replace <compose> for:
<require from="parentForm"></require>
<parent-form></parent-form>
Use kebab-case instead of camel-case to name your files. For example, instead of parentForm.html and parentForm.js, use parent-form.html and parent-form.js. This won't change a thing in your code but you will be following nice javascript standards :)
When binding directly to a string you don't need to use .bind. For example, view-model.bind="'parentForm'" could be replaced for view-model="./parentForm"
Hope this helps!
One thing that immediately comes to mind is that you can inject the parent model into your child model in the constructor -- the injected instance will be the same, not a newly created one. This way, your parent can define a method that allows the child to register itself on the parent, and the parent can then invoke whatever methods exist on the child at the time of its choosing.
This creates a rather strong coupling between the components, though, so you will need to consider whether or not that is acceptable to you.
If it isn't, another way to approach the issue is to use the event aggregator. The parent form can dispatch an event on the aggregator, and the children will be subscribers listening for the event. In this case, depending on whether or not you host multiple such combinations on one page, you may want to include a unique identifier for the form that is sent along with the event and bind that ID to the child components, so they will know to only listen for events from their parent.
I am trying to build a filter for my portfolio website. Checkboxes that let you pick a technology (react, redux, jquery etc.) to display a piece of work(s) that contain(s) that/those technologies. So every time the user clicks on a box, I want to add the value (JavaScript, Redux, React etc.) to an array that I use in another function to check against my portfolio pieces and filter out what isn't there.
I am finding this very difficult and I think it should be quite simple. Can someone point me in the right direction? Is there a way to simply have a function trigger (onChange callback?) that reads the checked/unchecked status of my form input elements and then updates my state array accordingly? Can I get the status of all the checkboxes simply in React? Do I need to have individual state of checked/unchecked for my checkboxes?
It seems that jQuery makes it pretty possible with selectors with:
$('input[type="checkbox"]:checked').each(function () {}
If you don't care about the order and you just want to append the items to the array as they appear we could definitely do exactly what you suggest in your question. On the change event of the checkbox check if the box is checked or or unchecked (event.target.checked returns true if checked or false if unchecked) and handle the array logic accordingly. this is a simple representation of how that could work:
import React, { Component } from 'react'
import { connect } from 'react-redux'
class Portfolio extends Component {
constructor() {
super()
// initialize your options array on your state
this.state = {
options: []
}
}
onChange(e) {
// current array of options
const options = this.state.options
let index
// check if the check box is checked or unchecked
if (e.target.checked) {
// add the numerical value of the checkbox to options array
options.push(+e.target.value)
} else {
// or remove the value from the unchecked checkbox from the array
index = options.indexOf(+e.target.value)
options.splice(index, 1)
}
// update the state with the new array of options
this.setState({ options: options })
}
render() {
return (
<main className='portfolio'>
<form>
<div className="input-group">
<label>cb1</label>
<input type="checkbox" value={1} onChange={this.onChange.bind(this)} />
</div>
<div className="input-group">
<label>cb2</label>
<input type="checkbox" value={2} onChange={this.onChange.bind(this)} />
</div>
<div className="input-group">
<label>cb3</label>
<input type="checkbox" value={3} onChange={this.onChange.bind(this)} />
</div>
</form>
<div className="selected-items">
{this.state.options.map(number =>
<p key={number}>item: {number}</p>
)}
</div>
</main>
)
}
}
if you DO care about order, if you can append numerical values to the array like I did in this example you could easily give your checkboxes sorted numerical values and you could sort the array before updating your state so it's always in a certain order regardless of the order they are checked.
onChange(e) {
// current array of options
const options = this.state.options
let index
// check if the check box is checked or unchecked
if (e.target.checked) {
// add the numerical value of the checkbox to options array
options.push(+e.target.value)
} else {
// or remove the value from the unchecked checkbox from the array
index = options.indexOf(+e.target.value)
options.splice(index, 1)
}
// sort the array
options.sort()
// update the state with the new array of options
this.setState({ options: options })
}
Here's how I'm doing it:
// util.js
import getPath from 'lodash/get';
import setIn from 'lodash/fp/set';
export function linkMultiCheck(name, value) {
return {
checked: getPath(this.state, name, []).includes(value),
onChange: ev => {
let values = getPath(this.state, name, []);
if(ev.target.checked) {
values = [...values, value];
} else {
values = values.filter(v => v !== value);
}
this.setState(setIn(name, values));
},
}
}
// form.js
<ul>
{options.branches.map(branch => (
<li key={branch.id} className="checkbox">
<label>
<input type="checkbox" name={this.id} {...this::linkMultiCheck('formData.branchIds',branch.id)}/>
{branch.id}
</label>
</li>
))}
</ul>
i.e., if a checkbox is checked, append it to the current array of values. If it's unchecked, filter it out.
I'm using lodash here so that we can set deeply nested state values using dot notation.
All:
I wonder if it is possible that binding multiple event handlers to same event?
For example:
var LikeToggleButton = React.createClass({
render: function(){
(function toggle(){
this.setState({liked:!like});
}).bind(this);
return (
<div onClick={toggle}>TOGGLE LIKE</div>
);
}
});
Until this point everything seems normal, but I want to add another feature to that button, which is decide by other option:
For example, I have another switch component(could be anything like checkbox or radio button etc.) called "count toggle", which when enabled, the LikeToggleButton's button will be added another onClick handler which is start counting times of button clicked, I know it could be predesignd into the toggle function, but I just wonder if there is a way to append this part to onClick handler?
Thanks
If you want to have multiple callbacks executed when onClick is triggered, you can have them passed from outside, so you'll have access to them in the props object. Then execute them all (note: code not tested):
var LikeToggleButton = React.createClass({
toggle: function() {
this.setState({liked:!like});
},
handleClick: function(e) {
e.preventDefault();
this.toggle();
for (var i=0, l<this.props.callbacks.length; i<l; i++) {
this.props.callbacks[i].call();
}
},
render: function() {
return (
<div onClick={this.handleClick}>TOGGLE LIKE</div>
);
}
});
BUT, if you want to have components connected between them, you should not do that by calling methods inside handlers. Instead you should use an architectural pattern, where Flux is the obvious choice (but there are lots more).
Take a look to Flux, and here you have more choices.
For an extensible way that does't require the component to know about components that use it - save the onClick event before changing it.
This is highlights extracted from the actual working code:
button.jsx
class Button extends React.Component {
constructor(props) {
super(props);
this.state= { callback: false};
}
click(){
//do stuff here
if(this.state.callback) { this.state.callback.call(); }
}
render () {
this.state.callback = this.props.onClick; // save the onClick of previous handler
return (
<button { ...this.props } type={ this.props.type || "button" } onClick={ this.click.bind(this) } className = this.props.className } >
{ this.props.children }
</button>
);
}
}
export default Button;
Then in another component you can use the button and it can have it's own onClick handler:
class ItemButtons extends React.Component {
itemClick () {
//do something here;
}
render () {
const buttons = [
(
<Button onClick={ this.itemClick.bind(this) } className="item-button">
<span>Item-Button</span>
</Button>
)
];
return (<section>{ buttons }</section>);
}
export default ItemButtons;
To group multiple actions on an event
onMouseDown={(e) => { e.stopPropagation(); alert('hello'); }}
Maybe you can set multiple click event handlers on the same one target as described here: https://gist.github.com/xgqfrms-GitHub/a36b56ac3c0b4a7fe948f2defccf95ea#gistcomment-2136607
Code (copied from linke above):
<div style={{ display: 'flex' }}>
<div style={{
width: '270px',
background: '#f0f0f0',
borderRight: "30px solid red",
minHeight: ' 500px',
maxHeight: '700px',
overflowX: 'hidden',
overflowY: 'scroll',
}}
onClick={this.state.ClickHandler}
onClick={this.stateHandleClick}
className="sidebar-btn"
>
<button onClick={this.props.ClickHandler}>props</button>
<button onClick={(e) => this.props.ClickHandler}>props</button>
<button onClick={this.props.ClickHandler}>props</button>
<button onClick={this.state.ClickHandler}>state</button>
//...
</div>
I'm trying to wrap Semantic UI Modal component using portal approach described here
Here is my take at it http://jsfiddle.net/mike_123/2wvfjpy9/
I'm running into issue though, when obtaining a DOM reference and Rendering new markup into it there seem to be old reference still maintained.
render: function() {
return <div className="ui modal"/>; <-- the idea at first was to only return <div/>
},
...
React.render(<div > <----------- originally this element had className="ui modal", but this.node doesn't seem to overtake the original node reference
<i className="close icon"></i>
<div className="header">test</div>
<div className="content">
{props.children}
</div>
</div>, <-----------
this.node);
Any pointers how fix this test case http://jsfiddle.net/mike_123/2wvfjpy9/
You will lose correct vertical positioning and probably animations with approaches mentioned above.
Instead, you can just place your modal's component inside your app's root component and call .modal() with detachable: false. With this option, semantic wouldn't make any DOM manipulations and you won't lose your React DOM event listeners.
Example using Webpack/Babel:
import React, { Component } from 'react'
import $ from 'jquery'
if (typeof window !== 'undefined') {
window.jQuery = $
require('semantic-ui/dist/semantic.js')
}
class App extends Component {
state = {
showModal: false
}
_toggleModal = (e) => {
e.preventDefault()
this.toggleModalState()
}
toggleModalState = () => {
this.setState({ showModal: !this.state.showModal })
}
render() {
return (
<div>
<a href="" onClick={this._toggleModal}></a>
{this.state.showModal
? <Modal toggleModalState={this.toggleModalState}/>
: ''}
</div>
)
}
}
class Modal extends Component {
componentDidMount() {
$(this.modal)
.modal({ detachable: false })
.modal('show')
}
componentWillUnmount() {
$(this.modal).modal('hide')
}
_close = (e) {
e.preventDefault()
alert("Clicked")
this.props.toggleModalState()
}
render() {
return (
<div ref={(n) => this.modal = n} className="ui modal">
<div class="content">
<a onClick={this._close} href="">Click Me</a>
</div>
</div>
)
}
}
When you call this.$modal.modal('show'), it will actually restructure your DOM, and React will not be happy about it. Plus, if you try to put control in your modal, the control will not work.
What you should do is to React.render an already shown modal, i.e. a modal with markup as if $('.ui.modal').modal('show') has been called.
Here is my attempt using "React-Portal" to help with rendering a react component at body level. You can still use your method if you prefer.
// modal.jsx
import React, { Component } from 'react';
import Portal from 'react-portal';
class InnerModal extends Component {
constructor(props) {
super(props);
this.state = { modalHeight: 0 };
}
componentDidMount() {
let modalHeight = window.$('#reactInnerModal').outerHeight();
this.setState({modalHeight: modalHeight});
}
render() {
return (
<div id='reactInnerModal' className='ui standard test modal transition visible active' style={{'margin-top': - this.state.modalHeight / 2}}>
<i className='close icon' onClick={this.props.closePortal}></i>
{this.props.children}
</div>
);
}
}
class Modal extends Component {
render() {
let triggerButton = <button className='ui button'>Open Modal</button>;
return (
<Portal className='ui dimmer modals visible active page transition' openByClickOn={triggerButton} closeOnEsc={true} closeOnOutsideClick={true}>
<InnerModal>
{this.props.children}
</InnerModal>
</Portal>
);
}
}
export default Modal;
Notice that my modal has already been rendered in the markup.
You can then consume the modal as below:
// index.jsx
import React, { Component } from 'react';
import Modal from './modal';
class ModalDemo extends Component {
render() {
return (
<Modal>
<div className='header'>
Profile Picture
</div>
<div className='image content'>
<div className='ui medium image'>
<img src='http://semantic-ui.com/images/avatar2/large/rachel.png' />
</div>
<div className='description'>
<div className="ui header">We've auto-chosen a profile image for you.</div>
<p>We've grabbed the following image from the <a href='https://www.gravatar.com' target='_blank'>gravatar</a> image associated with your registered e-mail address.</p>
<p>Is it okay to use this photo?</p>
</div>
</div>
<div className='actions'>
<div className='ui black deny button'>
Nope
</div>
<div className='ui positive right labeled icon button'>
Yep, that's me
<i className='checkmark icon'></i>
</div>
</div>
</Modal>
);
}
}
React.render(<ModalDemo />, document.getElementById('content'));
With this you don't have to hack your way into DOM manipulation with jQuery, and the control in the modal (button, link, etc, to invoke functions) still works.
Hope this help!
Khanetor answered this question thoroughly and correctly, I just want to contribute one additional tidbit about how to position the Modal. It would be best as a comment, but unfortunately, I don't have the reputation to do so.
Anyways, the first child element of the Portal element needs to be positioned absolutely in order to make the dimmer and resulting modal sit on top of the page content rather than get put beneath it.
First, add style={position:'absolute'} to the Portal declaration in Modal's render method so the dimmer gets set at the top of the page. You end up with:
<Portal className='ui dimmer modals visible active page transition' openByClickOn={triggerButton} closeOnEsc={true} closeOnOutsideClick={true} style={position:'absolute'}>
<InnerModal>
{this.props.children}
</InnerModal>
</Portal>
Next, set the InnerModal's position to relative and decide on a distance from the top of the screen. I used an eighth (or 0.125) of the browser's viewport and got:
class InnerModal extends Component {
constructor(props){
super(props);
this.state = {
modalId : _.uniqueId('modal_'),
style: {}
}
}
componentDidMount() {
this.setState({
style : {
position : 'relative',
top : $(window).height() * 0.125 + 'px'
}
});
}
render(){
return (
<div id={this.state.modalId} className='ui standard modal transition visible active'
style={this.state.style}>
<i className='close icon' onClick={this.props.closePortal}></i>
{ this.props.children }
</div>
);
}
}
With those edits made, I've finally got some working modals in React! Hope this is helpful to someone else running into some of the same issues I've been.