ALM ALM - 6 months ago 58
Javascript Question

How do I get the value in the parent from a child components <select> in ReactJS onSubmit()

I am having an issue trying to get the value from a

<select>
. The component which renders it is
DualListBox
which should add a
ref
of
selected
to the
<select>
tag.

<div className="rdl-selected">
<select className="form-control" name={this.props.name} ref="selected" multiple>
{selected}
</select>
</div>


The problem is that in the parent component of
UpdateDialog
I am unable to get the value for the DOM when I click save on the page in the
handleSubmit


ReactDOM.findDOMNode(this.refs['selected']).value.trim();


Question:

What am I missing? I'm wondering if the component needs to be wrapped with something to return the value correctly.

Update
Added changes as noted by @Klassman for passing onChange and it works

This way the passed down onChange is called with the state value. Thank You

** Is there another way to do this not passing the value within the state?**
** Why was I unable to reference it by the ref?**

Thank you

Code

UpdateDialog

'use strict';

const React = require('react');
const ReactDOM = require('react-dom');

const when = require('when');
const client = require('./client');
const follow = require('./follow'); // function to hop multiple links by "rel"


const ListBoxWidget = require('./ListBoxWidget');

const root = '/api';

class UpdateDialog extends React.Component {

constructor(props) {
super(props);
this.state = {selectedListObjects: null, unselectedListObjects: null};
this.handleSubmit = this.handleSubmit.bind(this);
this.onChange = this.onChange.bind(this);
this.setupTestSuite = this.setupTestSuite.bind(this);
}

onChange(selected) {
this.setState({ selected });
}

handleSubmit(e) {
e.preventDefault();
var updatedObject = {};
this.props.attributes.forEach(attribute => {
var rd = ReactDOM.findDOMNode(this.refs[attribute]).value.trim();
console.log("rd for attribute " + attribute);
console.log(this.refs[attribute]);
console.log(rd);
updatedObject[attribute] = rd;
});


console.log("going into test suite ");
console.log(this.props.objectName);

// save the list box value for the test cases
if (this.props.objectName == "Test Suite") {
console.log("going into test suite ");
console.log("this.refs['selected']");
console.log(this.refs.selected);
console.log(this.refs.available);

console.log("this.state.selected");
console.log(this.state.selected);
console.log("?");
var selectedTestCases = ReactDOM.findDOMNode(this.refs['selected']).value.trim();
console.log("selected react dom");
console.log(selectedTestCases);
if (selectedTestCases != null) {
console.log("selected react dom . value");
console.log(selectedTestCases.value);
}

}
this.props.onUpdate(this.props.object, updatedObject);
window.location = "#";
}

componentDidMount() {
console.log("calling componentDidMount in update");

if (this.props.objectName == "Test Suite") {
this.setupTestSuite();
}
}

render() {
var inputs = this.props.attributes.map(attribute =>
<p key={attribute}>
<input type="text" placeholder={attribute}
defaultValue={this.props.object.entity[attribute]}
ref={attribute} className="field" />
</p>
);

var dialogId = "updateObject-" + this.props.object.entity._links.self.href;

return (
<div>
<a href={"#" + dialogId}>Update</a>

<div id={dialogId} className="modalDialog">
<div>
<a href="#" title="Close" className="close">X</a>

<h2>Update an {this.props.objectName}</h2>

<form>
{inputs}
{this.props.objectName == "Test Suite" && this.state.selectedListObjects != null && this.state.unselectedListObjects != null &&
<ListBoxWidget
onChange={this.onChange}
refName='selectedTestCasesRef'
name='selectedTestCases'
unselectedListObjects={this.state.unselectedListObjects}
selectedListObjects={this.state.selectedListObjects} />
}
<button onClick={this.handleSubmit}>Update</button>
</form>
</div>
</div>
</div>
)
}

}

module.exports = UpdateDialog


ListBoxWidget

'use strict';

const React = require('react');

// import components
const DualListBox = require('react-dual-listbox');

class ListBoxWidget extends React.Component {
constructor(props) {
super(props);
this.state = { selected: this.props.selectedListObjects };
this.onChange = this.onChange.bind(this);
}

onChange(selected) {
this.setState({ selected });
this.props.onChange(selected);
}

render() {

return (
<DualListBox
name={this.props.name}
options={this.props.unselectedListObjects}
preserveSelectOrder
selected={this.state.selected}
defaultValue={this.props.selectedListObjects}
onChange={this.onChange} />

)
}
}

module.exports = ListBoxWidget


Package.json

"babel-core": "^6.0.0",
"babel-loader": "^6.0.0",
"babel-preset-es2015": "^6.6.0",
"babel-preset-react": "^6.5.0",
"react": "^15.0.1",
"react-dom": "^15.0.1",
"rest": "^1.3.2",
"sockjs-client": "^1.0.3",
"stompjs": "^2.3.3",
"webpack": "^1.13.0",
"when": "^3.7.7",
"react-bootstrap": "^0.29.3",
"jquery": "^2.2.3",
"react-dual-listbox": "^0.3.3"


Here is the class from react-dual-listbox

DualListBox

import React from 'react';

import Action from './Action';

class DualListBox extends React.Component {
static propTypes = {
name: React.PropTypes.string,
options: React.PropTypes.array,
available: React.PropTypes.array,
selected: React.PropTypes.array,
onChange: React.PropTypes.func,
preserveSelectOrder: React.PropTypes.bool,
};

/**
* @param {Object} props
*
* @returns {void}
*/
constructor(props) {
super(props);

this.onClick = this.onClick.bind(this);
this.onDoubleClick = this.onDoubleClick.bind(this);
}

/**
* @param {Object} event
*
* @return {void}
*/
onClick(event) {
const { target } = event;
const { options, onChange } = this.props;
const direction = target.dataset.moveDirection;
const isMoveAll = target.dataset.moveAll;
const selectRef = direction === 'right' ? 'available' : 'selected';

let selected = [];

if (isMoveAll === '1') {
selected = direction === 'right' ? this.makeOptionsSelected(options) : [];
} else {
selected = this.toggleSelected(
this.getSelectedOptions(this.refs[selectRef])
);
}

onChange(selected);
}

/**
* @param {Object} event
*
* @returns {void}
*/
onDoubleClick(event) {
const value = event.target.value;
const selected = this.toggleSelected([value]);

this.props.onChange(selected);
}

/**
* Converts a flat array to a key/value mapping.
*
* @param {Array} options
*
* @returns {Object}
*/
getLabelMap(options) {
let labelMap = {};

options.forEach((option) => {
if (option.options !== undefined) {
labelMap = { ...labelMap, ...this.getLabelMap(option.options) };
} else {
labelMap[option.value] = option.label;
}
});

return labelMap;
}

/**
* Returns the selected options from a given element.
*
* @param {Object} element
*
* @returns {Array}
*/
getSelectedOptions(element) {
return [...element.options]
.filter((option) => option.selected)
.map((option) => option.value);
}

/**
* Make all the given options selected.
*
* @param {Array} options
*
* @returns {Array}
*/
makeOptionsSelected(options) {
let selected = [];

this.filterAvailable(options).forEach((option) => {
if (option.options !== undefined) {
selected = [...selected, ...this.makeOptionsSelected(option.options)];
} else {
selected.push(option.value);
}
});

return [...this.props.selected, ...selected];
}

/**
* Toggle a new set of selected elements.
*
* @param {Array} selected
*
* @returns {Array}
*/
toggleSelected(selected) {
const oldSelected = this.props.selected.slice(0);

selected.forEach((value) => {
const index = oldSelected.indexOf(value);

if (index >= 0) {
oldSelected.splice(index, 1);
} else {
oldSelected.push(value);
}
});

return oldSelected;
}

/**
* Filter options by a filtering function.
*
* @param {Array} options
* @param {Function} filterer
*
* @returns {Array}
*/
filterOptions(options, filterer) {
const filtered = [];

options.forEach((option) => {
if (option.options !== undefined) {
const children = this.filterOptions(option.options, filterer);

if (children.length > 0) {
filtered.push({
label: option.label,
options: children,
});
}
} else if (filterer(option)) {
filtered.push(option);
}
});

return filtered;
}

/**
* Filter the available options.
*
* @param {Array} options
*
* @returns {Array}
*/
filterAvailable(options) {
if (this.props.available !== undefined) {
return this.filterOptions(options, (option) =>
this.props.available.indexOf(option.value) >= 0 &&
this.props.selected.indexOf(option.value) < 0
);
}

// Show all un-selected options
return this.filterOptions(options, (option) =>
this.props.selected.indexOf(option.value) < 0
);
}

/**
* Filter the selected options.
*
* @param {Array} options
*
* @returns {Array}
*/
filterSelected(options) {
if (this.props.preserveSelectOrder) {
return this.filterSelectedByOrder(options);
}

// Order the selections by the default order
return this.filterOptions(options, (option) =>
this.props.selected.indexOf(option.value) >= 0
);
}

/**
* Preserve the selection order. This drops the opt-group associations.
*
* @param {Array} options
*
* @returns {Array}
*/
filterSelectedByOrder(options) {
const labelMap = this.getLabelMap(options);

return this.props.selected.map((selected) => ({
value: selected,
label: labelMap[selected],
}));
}

/**
* @returns {Array}
*/
renderOptions(options) {
return options.map((option, index) => {
if (option.options !== undefined) {
return (
<optgroup key={index} label={option.label}>
{this.renderOptions(option.options)}
</optgroup>
);
}

return (
<option key={index} value={option.value} onDoubleClick={this.onDoubleClick}>
{option.label}
</option>
);
});
}

/**
* @returns {React.Component}
*/
render() {
const { options } = this.props;
const available = this.renderOptions(this.filterAvailable(options));
const selected = this.renderOptions(this.filterSelected(options));

return (
<div className="react-dual-listbox">
<div className="rdl-available">
<select className="form-control" ref="available" multiple>
{available}
</select>
</div>
<div className="rdl-actions">
<div className="rdl-actions-right">
<Action direction="right" isMoveAll onClick={this.onClick} />
<Action direction="right" onClick={this.onClick} />
</div>
<div className="rdl-actions-left">
<Action direction="left" onClick={this.onClick} />
<Action direction="left" isMoveAll onClick={this.onClick} />
</div>
</div>
<div className="rdl-selected">
<select className="form-control" name={this.props.name} ref="selected" multiple>
{selected}
</select>
</div>
</div>
);
}
}

export default DualListBox;

Answer

You could pass on the onChange callback from the ListBoxWidget to the UpdateDialog

simplified example:

class UpdateDialog extends React.Component {

  onChange (selected) {
    // tadaa! `selected` has passed on to this component
    this.setState({ selected })
  }

  handleSubmit () {
    // instead of the `ReactDOM.findDOMNode` stuff, just point to your state:
    const selected = this.state.selected
    // ...
  }

  render () {
    return <ListBoxWidget onChange={this.onChange} />
  }
}


class ListBoxWidget extends React.Component {

  onChange (selected) {
    this.setState({ selected })
    this.props.onChange(selected)
  }

  render () {
    return <DualListBox onChange={onChange} />
  }
}
Comments