Conditional show/hide of autogenerated form elements in ReactJS - javascript

I have a ReactJS app where I am
A) reading a JSON input that describes a form's structure
B) dynamically generating a form from this JSON input (using document.createElement(..))
The JSON would look something like this:
{
formElements: [
{
id: “dd1”,
type: “dropdown”,
options: [ {value: “first”}, {value: “second”}]
},
{
id: “tf1”,
type: “textfield”,
showIf: “dd1 == ‘second’”
}
]
}
Now the tricky thing is that the JSON input file not only describes which form elements (e.g. dropdown, radio button group, text field) etc should be present but it ALSO describes show/hide logic for each element. For example, if a particular dropdown selection is made, then a textfield should be shown (otherwise it should stay hidden).
This would normally be done in jQuery but I have heard jQuery is not a good idea with React.
If these were hardcoded form elements, I could easily code this show/hide logic. The problem is that the form elements are being dynamically generated (by reading that JSON file) and I need to apply this show/hide logic on the fly to these autogenerated form elements.
I'm not sure how to do this.
If any one has suggestions for approaches here, especially with examples, that would be much appreciated. Thank you!

You should still be able to apply conditional rendering logic to the JSX code that is generating your form, but have you looked into using an existing form library like react-form or redux-forms? If you're relatively new to react, this would be a much easier route to get the results you want. I can't recommend a particular form library, but react-form notes that it handles dynamic data.
Here is my rough sketch of how you could manage this without using redux or a built-in form library. This is a draft that was imagined but never executed, so treat it like psuedo-code and definitely not optimized:
//import base form components up here (input, checkbox, etc)
// Map the strings in your field object to the component imported or defined above
const fieldSelector = {
input : Input,
textarea: TextArea,
checkbox: CheckBox
}
Class CustomForm extends React.Component {
constructor(props) {
super(props);
const fields = {}
const byId = []
// Note if there is any asynchronous data, you might want to put this logic
// in componentDidMount
// Create an array with each 'id'
const byId = this.props.formData.map( item => item.id );
// Create a map object listing each field by its id
this.props.formData.forEach( (field) => {
fields[field.id] = field;
}
this.state = {
fields,
byId,
}
this.handleChange = this.handleChange.bind(this);
this.checkVisibility = this.checkVisibility.bind(this);
}
// Need to add some additional logic if you're using checkboxes
// Creates an event handler for each type of field
handleChange(id) {
return (event) => {
const updatedFields = {...this.state.fields};
updatedFields[id].value = event.target.value
this.state.byId.forEach( fieldId => {
updatedFields[fieldId].visible = checkVisibility(updatedFields, fieldId)};
}
this.setState({fields: updatedFields})
}
}
// You can either restructure your showIf or include a function above
// to parse our the elements of the expression.
checkVisibility(updatedFields, fieldId) {
const field = updatedFields[fieldId];
const showIfId = field.showIf.triggerFieldId;
const showIfValue = field.showIf.value;
const operator = field.showIf.operator;
switch(operator){
case '===':
return updatedFields[showIfId].value === ShowIfValue;
case '<':
return updatedFields[showIfId].value < ShowIfValue;
//...fill in rest of operators here
default:
return field.visible;
}
}
render() {
return this.state.byId.map( fieldId => {
const field = this.state.fields[fieldId];
const CustomField = FieldSelector[field.type]
return (
{field.visible &&
<CustomField {insert whatever props from field} />
}
);
});
}
}
export default CustomForm

Related

Can you use a backdraftjs watchable to make a component completely re-render?

This is a contrived example but it is similar to real-life situations where, for example, you might have a list of links built from data that you are AJAXing in from a server.
import {Component, e, render} from './node_modules/bd-core/lib.js';
// a list of strings that for alert when you click them
class AlertLinkList extends Component.withWatchables('data') {
handleClick(event){
alert(event.target.innerHTML);
}
bdElements() {
return e.div(
{
// bdReflect: ?????
},
['yes', 'no', 'maybe'].map(
txt => e.div({bdAdvise: {click: 'handleClick'}}, txt)
)
)
}
}
var linkList = render(AlertLinkList, {}, document.body);
// I would like to change the strings but this (obviously) does nothing
linkList.data = ['soup', 'nuts', 'fish', 'dessert'];
I can't think of a straightforward way to solve this.
bdReflect only works on writable DOM attributes, I think, so for example I could use it to replace the innerHTML of the component but then I think I lose the bdAdvise assignments on the links (and it also seems kinda kludgey).
Any ideas?
OK here's one pattern that works for this...
get rid of the watchables in AlertLinkList
instead, use kwargs to populate the list
wrap the list in another component that simply re-renders the list with new content whenever the content changes (e.g. after fetching new content from the server)
// a list of strings that alert when you click them
class AlertLinkList extends Component {
handleClick(event){
alert(event.target.innerHTML);
}
bdElements() {
return e.div(
this.kwargs.items.map(
txt => e.div({bdAdvise: {click: 'handleClick'}}, txt)
)
)
}
}
// a wrapper that provides/retrieves data for AlertLinkList
class LinkListWrapper extends Component {
bdElements() {
return e.div(
{},
e.a(
{bdAdvise: {click: 'updateList'}},
'Click to Update List',
),
e.div({bdAttach: 'listGoesHere'}),
);
}
updateList(event) {
// the data below would have been retrieved from the server...
const resultRetrievedFromServer = ['soup', 'nuts', 'fish', 'dessert'];
this.renderList(resultRetrievedFromServer)
}
renderList(items) {
render(AlertLinkList, {items}, this.listGoesHere, 'only')
}
postRender() {
const initialData = ['yes', 'no', 'maybe']
this.renderList(initialData);
}
}
var linkList = render(LinkListWrapper, {}, document.body);
The only issue I see here is that it may be suboptimal to re-render the entire wrapped component if only one small part of the data changed, though I suppose you could design around that.
Let's begin solving this problem by describing the public interface of AlertLinkList:
A component that contains a homogeneous list of children.
The state of each child is initialized by a pair of [text, url].
The list is mutated en masse.
Given this, your start is almost perfect. Here it is a again with a few minor modifications:
class AlertLinkList extends Component.withWatchables('data') {
handleClick(event) {
// do something when one of the children is clicked
}
bdElements() {
return e.div({}, this.data && this.data.map(item => e(AlertLink, { data: item })));
}
onMutateData(newValue) {
if (this.rendered) {
this.delChildren();
newValue && newValue.forEach(item => this.insChild(AlertLink, { data: item }));
}
}
}
See https://backdraftjs.org/tutorial.html#bd-tutorial.watchableProperties for an explanation of onMutateData.
Next we need to define the AlertLink component type; this is trivial:
class AlertLink extends Component {
bdElements() {
return e.a({
href: this.kwargs.data[1],
bdAdvise: { click: e => this.parent.handleClick(e) }
}, this.kwargs.data[0]);
}
}
The outline above will solve your problem. I've written the pen https://codepen.io/rcgill/pen/ExWrLbg to demonstrate.
You can also solve the problem with the backdraft Collection component https://backdraftjs.org/docs.html#bd-core.classes.Collection
I've written a pen https://codepen.io/rcgill/pen/WNpmeyx to demonstrate.
Lastly, if you're interested in writing the fewest lines of code possible and want a fairly immutable design, you don't have to factor out a child type. Here's a pen to demonstrate that: https://codepen.io/rcgill/pen/bGqZGgW
Which is best!?!? Well, it depends on your aims.
The first solution is simple and general and the children can be wrangled to do whatever you want them to do.
The second solution is very terse and includes a lot of additional capabilities not demonstrated. For example, with the backdraft Collection component
mutating the collection does not destroy/create new children, but rather alters the state of existing children. This is much more efficient and useful when implementing things like large grids.
you can mutate an individual elements in the collection
The third solution is very terse and very fixed. But sometimes that is all you need.

How can I bind a text box to a complex object within state in React?

Background
I am building an office add-in using their React-based starter kit and TypeScript. I mention this because I am unable to get great debug support as far as I can tell, so I'm unable to see the error message that React is providing in my current situation.
What I'm attempting
(simplifying below. I can be more specific if you'd like; let me know.)
I have an interface for my AppState, and a complex object with some properties:
export interface AppState {
eventInput: EventInput;
}
export class BookendEventInput {
public minutesAdjacent: number = 30;
public subject: string = "";
public enabled: boolean = false;
public eventId: string = "";
}
I have one working scenario, which is a checkbox:
<Checkbox id="enableBookendBefore" checked={this.state.eventInput.enabled} onChange={this.eventInputCheckboxChanged}></Checkbox>
That is updating the state via the change function:
eventInputCheckboxChanged = () => {
this.setState((state: AppState) => {
var newValue = !this.state.eventInput.enabled;
var input = state.eventInput;
input.enabled = newValue;
state.eventInput = input;
})
}
But this isn't working for another scenario.
The Problem
I am now attempting to do something similar with a textbox. I have an input:
<input type="text" id="subject" disabled={!this.state.eventInput.enabled} value={this.state.eventInput.subject} onChange={this.subjectChanged} />
And the change function:
subjectChanged = (e) => {
var newSubject = e.target.value;
this.setState((state: AppState)=> {
var input = state.eventInput;
input.subject = newSubject;
state.eventInput = input;
})
Expected Behavior: I would expect to see the subject text box & state updated, as if they were two-way bound.
Actual Behavior: The entire screen goes blank/white, indicating that I'm getting a React-level error I believe (since I can't do F12 and can't see debug output due to it being in a task pane inside Outlook.)
The Question
How can I correctly bind a textbox using React, that's tied to a property in an object within state? Is it possible to do this, or am I violating a React principle?
In this case, you're using the callback to setState to try and modify state. This is either not firing or causing an infinite loop, I'm unsure of which!
Either way, to correctly modify state you'll want:
subjectChanged = (e) => {
var newSubject = e.target.value;
var input = state.eventInput;
input.subject = newSubject;
this.setState({eventInput: input});
});
This will achieve what you're looking for.

how to pass objects in web component javascript [duplicate]

My understanding is that data is passed to a custom html element via its attributes and sent out by dispatching a CustomEvent.
JavaScript objects can obviously be sent out in the event's detail field, but what if the element needs a lot of data passed into it. Is there a way to provide it with an object in JavaScript.
What if the element for instance contains a variable number of parts that needs to be initialized or changed dynamically (e.g. a table with a variable number of rows)? I can imagine setting and modifying an attribute consisting of a JSON string that is parsed inside the component, but it does not feel like an elegant way to proceed:
<my-element tableRowProperties="[{p1:'v1', p2:'v2'}, {p1:'v1',p2:'v2'}, {p1:'v1',p2:'v2'}]"></my-element>
Or can you make the element listen to events from the outside that contains a payload of data?
Passing Data In
If you really want/need to pass large amounts of data into your component then you can do it four different ways:
1) Use a property. This is the simplest since you just pass in the Object by giving the value to the element like this: el.data = myObj;
2) Use an attribute. Personally I hate this way of doing it this way, but some frameworks require data to be passed in through attributes. This is similar to how you show in your question. <my-el data="[{a:1},{a:2}....]"></my-el>. Be careful to follow the rules related to escaping attribute values. If you use this method you will need to use JSON.parse on your attribute and that may fail. It can also get very ugly in the HTML to have the massive amount of data showing in a attribute.
3 Pass it in through child elements. Think of the <select> element with the <option> child elements. You can use any element type as children and they don't even need to be real elements. In your connectedCallback function your code just grabs all of the children and convert the elements, their attributes or their content into the data your component needs.
4 Use Fetch. Provide a URL for your element to go get its own data. Think of <img src="imageUrl.png"/>. If your already has the data for your component then this might seem like a poor option. But the browser provides a cool feature of embedding data that is similar to option 2, above, but is handled automatically by the browser.
Here is an example of using embedded data in an image:
img {
height: 32px;
width: 32px;
}
<img src="data:image/svg+xml;charset=utf8,%3C?xml version='1.0' encoding='utf-8'?%3E%3Csvg version='1.1' xmlns='http://www.w3.org/2000/svg' x='0px' y='0px' viewBox='0 0 314.7 314.7'%3E%3Cstyle type='text/css'%3E .st0{fill:transparent;stroke:%23231F20;stroke-width:12;} .st1{fill:%23231F20;stroke:%23231F20;stroke-width:10;stroke-linejoin:round;stroke-miterlimit:10;} %3C/style%3E%3Cg%3E%3Ccircle class='st0' cx='157.3' cy='157.3' r='150.4'/%3E%3Cpolygon class='st1' points='108,76.1 248.7,157.3 108,238.6'/%3E%3C/g%3E%3C/svg%3E">
And here is an example of using embedded data in a web component:
function readSrc(el, url) {
var fetchHeaders = new Headers({
Accept: 'application/json'
});
var fetchOptions = {
cache: 'default',
headers: fetchHeaders,
method: 'GET',
mode: 'cors'
};
return fetch(url, fetchOptions).then(
(resp) => {
if (resp.ok) {
return resp.json();
}
else {
return {
error: true,
status: resp.status
}
}
}
).catch(
(err) => {
console.error(err);
}
);
}
class MyEl extends HTMLElement {
static get observedAttributes() {
return ['src'];
}
attributeChangedCallback(attrName, oldVal, newVal) {
if (oldVal !== newVal) {
this.innerHtml = '';
readSrc(this, newVal).then(
data => {
this.innerHTML = `<pre>
${JSON.stringify(data,0,2)}
</pre>`;
}
);
}
}
}
// Define our web component
customElements.define('my-el', MyEl);
<!--
This component would go load its own data from "data.json"
<my-el src="data.json"></my-el>
<hr/>
The next component uses embedded data but still calls fetch as if it were a URL.
-->
<my-el src="data:json,[{"a":9},{"a":8},{"a":7}]"></my-el>
You can do that same this using XHR, but if your browser supports Web Components then it probably supports fetch. And there are several good fetch polyfills if you really need one.
The best advantage to using option 4 is that you can get your data from a URL and you can directly embed your data. And this is exactly how most pre-defined HTML elements, like <img> work.
UPDATE
I did think of a 5th way to get JSON data into an object. That is by using a <template> tag within your component. This still required you to call JSON.parse but it can clean up your code because you don't need to escape the JSON as much.
class MyEl extends HTMLElement {
connectedCallback() {
var data;
try {
data = JSON.parse(this.children[0].content.textContent);
}
catch(ex) {
console.error(ex);
}
this.innerHTML = '';
var pre = document.createElement('pre');
pre.textContent = JSON.stringify(data,0,2);
this.appendChild(pre);
}
}
// Define our web component
customElements.define('my-el', MyEl);
<my-el>
<template>[{"a":1},{"b":"<b>Hi!</b>"},{"c":"</template>"}]</template>
</my-el>
Passing Data Out
There are three ways to get data out of the component:
1) Read the value from a property. This is ideal since a property can be anything and would normally be in the format of the data you want. A property can return a string, an object, a number, etc.
2) Read an attribute. This requires the component to keep the attribute up to date and may not be optimal since all attributes are strings. So your user would need to know if they need to call JSON.parse on your value or not.
3) Events. This is probably the most important thing to add to a component. Events should trigger when state changes in the component. Events should trigger based on user interactions and just to alert the user that something has happened or that something is available. Traditionally you would include the relevant data in your event. This reduces the amount of code the user of your component needs to write. Yes, they can still read properties or attributes, but if your events include all relevant data then they probably won't need to do anything extra.
There is a 6th way that is really similar to #Intervalia's answer above but uses a <script> tag instead of a <template> tag.
This is the same approach used by a Markdown Element.
class MyEl extends HTMLElement {
connectedCallback() {
var data;
try {
data = JSON.parse(this.children[0].innerHTML);
}
catch(ex) {
console.error(ex);
}
this.innerHTML = '';
var pre = document.createElement('pre');
pre.textContent = JSON.stringify(data,0,2);
this.appendChild(pre);
}
}
// Define our web component
customElements.define('my-el', MyEl);
<my-el>
<script type="application/json">[{"a":1},{"b":"<b>Hi!</b>"},{"c":"</template>"}]</script>
</my-el>
If you are using Polymer based web components, the passing of data could be done by data binding. Data could be stored as JSON string within attribute of and passed via context variable.
<p>JSON Data passed via HTML attribute into context variable of and populating the variable into combobox.</p>
<dom-bind><template>
<iron-ajax url='data:text/json;charset=utf-8,
[{"label": "Hydrogen", "value": "H"}
,{"label": "Oxygen" , "value": "O"}
,{"label": "Carbon" , "value": "C"}
]'
last-response="{{lifeElements}}" auto handle-as="json"></iron-ajax>
<vaadin-combo-box id="cbDemo"
label="Label, value:[[cbDemoValue]]"
placeholder="Placeholder"
items="[[lifeElements]]"
value="{{ cbDemoValue }}"
>
<template>
[[index]]: [[item.label]] <b>[[item.value]]</b>
</template>
</vaadin-combo-box>
<vaadin-combo-box label="Disabled" disabled value="H" items="[[lifeElements]]"></vaadin-combo-box>
<vaadin-combo-box label="Read-only" readonly value="O" items="[[lifeElements]]"></vaadin-combo-box>
<web-elemens-loader selection="
#polymer/iron-ajax,
#vaadin/vaadin-element-mixin/vaadin-element-mixin,
#vaadin/vaadin-combo-box,
"></web-elemens-loader>
</template></dom-bind>
<script src="https://cdn.xml4jquery.com/web-elements-loader/build/esm-unbundled/node_modules/#webcomponents/webcomponentsjs/webcomponents-loader.js"></script><script type="module" src="https://cdn.xml4jquery.com/web-elements-loader/build/esm-unbundled/src/web-elemens-loader.js"></script>
Using a tiny lib such as Lego would allow you to write the following:
<my-element :tableRowProperties="[{p1:'v1', p2:'v2'}, {p1:'v1',p2:'v2'}, {p1:'v1',p2:'v2'}]"></my-element>
and within your my-element.html web-component:
<template>
<table>
<tr :for="row in state.tableRowProperties">
<td>${row.p1}</td>
<td>${row.p2}</td>
</tr>
</template>
<script>
this.init() {
this.state = { tableRowPropoerties: [] }
}
</script>
I know this has been answered, but here is an approach I took. I know it's not rocket science and there are probably reasons not to do it this way; however, for me, this worked great.
This is an indirect approach to pass in data where an attribute called wc_data is passed in the custom element which is a 'key' that can be used one time.
You can obviously do whatever with the wc-data like callbacks and "callins" into the custom-tag.
link to codesandbox
files:
wc_data.ts
export const wc_data: {
[name: string]: any,
get(key: string): any,
set(key: string, wc_data: any): any
} = {
get(key: string): any {
const wc_data = this[key];
delete this[key];
return wc_data;
},
set(p_key: string, wc_data: any) {
this[p_key] = wc_data;
}
}
CustomTag.ts
import { wc_data } from './wc_data';
const template = document.createElement('template');
template.innerHTML = `
<style>
.custom-tag {
font-size: 1.6em;
}
</style>
<button class="custom-tag">Hello <span name="name"></span>, I am your <span name="relation"></span></button>
`;
class CustomTag extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.shadowRoot.appendChild(template.content.cloneNode(true));
}
callin() {
console.log('callin called');
}
connectedCallback() {
const v_wc_data = wc_data.get(this.getAttribute('wc-data'));
console.log('wc_data', v_wc_data);
const v_name = this.shadowRoot.querySelector('[name="name"]');
const v_relation = this.shadowRoot.querySelector('[name="relation"]');
v_name.innerHTML = v_wc_data.name;
v_relation.innerHTML = v_wc_data.relation;
const v_button = this.shadowRoot.querySelector('button');
v_button.style.color = v_wc_data.color;
v_wc_data.element = this;
v_button.addEventListener('click', () => v_wc_data.callback?.());
}
disconnectedCallback() {
}
}
window.customElements.define('custom-tag', CustomTag);
console.log('created custom-tag element');
export default {};
SomeTsFile.ts
wc_data.set('tq', {
name: 'Luke',
relation: 'father',
color: 'blue',
element: undefined,
callback() {
console.log('the callback worked');
const v_tq_element = this.element;
console.log(this.element);
v_tq_element.callin();
},
});
some html..
<div>stuff before..</div>
<custom-tag wc_data="tq" />
<div>stuff after...</div>
Thanks to the other contributors, I came up with this solution which seems somewhat simpler. No json parsing. I use this example to wrap the entire component in a-href to make the block clickable:
customElements.define('ish-marker', class extends HTMLElement {
constructor() {
super()
const template = document.getElementById('ish-marker-tmpl').content
const wrapper = document.createElement("a")
wrapper.appendChild( template.cloneNode(true) )
wrapper.setAttribute('href', this.getAttribute('href'))
const shadowRoot = this.attachShadow({mode: 'open'}).appendChild( wrapper )
}
})
<ish-marker href="https://go-here.com">
...
// other things, images, buttons.
<span slot='label'>Click here to go-here</span>
</ish-marker>

reactjs input element loses focus after keystroke

So I am using a hash to store the values of dynamically created rows of input values and I lose focus on the input I am modifying after entering only one character. I think the solution to this may be to use refs to refocus on only the last input changed, but I couldn't get it to work, as I wasn't able to figure out how to specify which element was last changed. Advice on how to solve this is appreciated.
The code below dynamically creates input boxes, and looks up their values based on the unitPriceValueHash. Each variant has an id, and id is used as the key to the hash.
I created a codepen to try and recreate the problem, but the issue im facing doesn't show up in code pen. In my actual app I press 1 for example in the input box, then the cursor is not on the input box anymore.
https://codepen.io/ByteSize/pen/oogLpE?editors=1011
The only difference between the codepen and my code appears to be the fact the the inputs are nested inside a table.
CreateItem(variant) {
const unitPriceValueHash = this.props.unitPriceValueHash
return {
variant_title: variant.variant_title,
variant_price: variant.variant_price,
unit_cost: <TextField
type="number"
onChange={(event) => this.handleUnitPriceChange(variant.id, event)}
key={variant.id}
value={unitPriceValueHash[variant.id] || ''}
/>
};
}
Below is the change of state that modifies the hash
handleUnitPriceChange (id, event) {
const unitPriceValueHash = this.state.unitPriceValueHash
unitPriceValueHash[id] = event
console.log(unitPriceValueHash)
this.setState({unitPriceValueHash: unitPriceValueHash});
//this.updateVariantUnitCost(id, event);
}
There's a couple problems with the code you've shared.
Don't use inline functions. Each render, the function is created again which means that when react compares the props, it looks like the function is different (it is a new/different function each time!) and react will re-render.
Don't modify any objects which exist in the state, instead create a new object. If you modify an object that exists in the state, you're essentially saying you don't want renders to be consistent and reproducible.
I've re-posted your original code with the issues highlighted
CreateItem(variant) {
const unitPriceValueHash = this.props.unitPriceValueHash
return {
variant_title: variant.variant_title,
variant_price: variant.variant_price,
unit_cost: <TextField
type="number"
onChange={(event) => this.handleUnitPriceChange(variant.id, event)}
// ^^^^ - inline functions cause react to re-render every time, instead - create a component
key={variant.id}
value={unitPriceValueHash[variant.id] || ''}
/>
};
}
handleUnitPriceChange(id, event) {
const unitPriceValueHash = this.state.unitPriceValueHash
unitPriceValueHash[id] = event
// ^^^^ - please, please - don't do this. You can't mutate the state like this.
// instead, do the following to create a new modified object without modifying the object in the state
const unitPriceValueHash = Object.assign({}, this.state.unitPriceValueHash, { id: event });
this.setState({ unitPriceValueHash: unitPriceValueHash });
}
In regards to the inline-function, generally the recommendation is to create a new component for this which takes the value as a prop. That might look like this:
class UnitCost extends PureComponent {
static propTypes = {
variantId: PropTypes.number,
variantValue: PropTypes.object,
onUnitPriceChange: PropTypes.func,
}
handleUnitPriceChange(e) {
this.props.onUnitPriceChange(this.props.variantId, e)
}
render() {
return (
<TextField
type="number"
onChange={this.handleUnitPriceChange}
value={this.props.variantValue || ''}
/>
);
}
}
CreateItem(variant) {
const unitPriceValueHash = this.props.unitPriceValueHash
return {
variant_title: variant.variant_title,
variant_price: variant.variant_price,
unit_cost: (
<UnitCost
key={variant.id}
variantId={variant.id}
variantValue={unitPriceValueHash[variant.id]}
onUnitPriceChange={this.handleUnitPriceChange}
/>
),
};
}
Regarding your concerns about focus, react generally won't lose your object focus when re-rendering, so don't ever, ever re-focus an object after an update for this reason.
The only time react will lose focus, is if it completely discards the current DOM tree and starts over from scratch. It will do this if it thinks a parent object has been replaced instead of modified. This can happen because of a missing key prop, or a key prop that has changed.
You have not posted enough code for us to investigate this further. If you want more help you should build a minimum reproducible example that we can run and test.
The solution to this problem had me use an intermediate state to store the value of the input field on change, and a submit AJAX request on an onBlur
class TextFieldWrapper extends React.Component {
constructor(props) {
super(props);
this.state = {
value: this.props.variantValue[this.props.variantId] || '',
}
this.handleUnitPriceChange = this.handleUnitPriceChange.bind(this)
this.updateValue = this.updateValue.bind(this)
}
updateValue(value){
this.setState({
value: value,
});
}
handleUnitPriceChange() {
this.props.onUnitPriceChange(this.props.variantId, this.state.value);
}
render(){
return (
<TextField
type="number"
id={this.props.variantId}
key={this.props.variantId}
onChange={this.updateValue}
onBlur={this.handleUnitPriceChange}
value={this.state.value}
/>
);
}
}

Rendering an array of html elements

I want to render an array of html elements in my component. The reason for storing the data/html in an array is because I want to be able to dynamically load a new element depending on a button-click.
This is how I want to display my array:
<div>
{this.state.steps}
</div>
This is how I initiate my component and array:
componentDidMount() {
this.createProcessStep().then(step => {
this.setState({steps: this.state.steps.concat(step)});
});
}
export function createProcessStep() {
this.setState({processStepCounter: this.state.processStepCounter += 1});
return this.addStepToArray().then(d => {
return this.reallyCreateProcessStep()
});
}
addStepToArray = () => {
const step = {
...Some variables...
};
return new Promise(resolve => {
this.setState({
stepsData: this.state.stepsData.concat(step)
}, resolve)
});
};
"stepsData" is another array that holds data (variables) belonging to each step. "steps" on the other hand, should only hold the html.
This is how one step/element looks like:
<div>
...Some Content...
<button label="+" onClick={ () => {
this.createProcessStep().then(step => {
this.setState({
steps: this.state.steps.concat(step)
});
})
}}/>
...other content...
</div>
This button within each step is responsible for loading/adding yet another step to the array, which actually works. My component displays each step properly, however react doesn't properly render changes to the element/step, which is
to say that, whenever e.g. I change a value of an input field, react doesn't render those changes. So I can actually click on the "+"-button that will render the new html element but whenever a change to this element occurs,
react simply ignores the phenotype of said change. Keeping in mind that the changeHandlers for those steps/elements still work. I can change inputfields, radioButtons, checkboxes etc. which will do exactly what it's
supposed to, however the "re-rendering" (or whatever it is) doesn't work.
Any ideas of what I'm doing wrong here? Thanks!
While you could certainly beat your approach into working, I would advise that you take more common react approach.
You make your components to correctly display themselves from the state . ie as many steps are in the state, your component will display. Than make your add button add necessary information (information, not formated html) to the state.
Here is an example how to use component N times:
const MyRepeatedlyOccuringComponent = (n) => (<p key={n}>There goes Camel {n}</p>)
const App = () => {
const camels = [1,22,333,4444,55555]
const caravan = camels.map((n) => MyRepeatedlyOccuringComponent(n))
return(<div>{caravan}</div>
}

Categories