react-jsonschema-form custom element position - reactjs

Using react-jsonschema-form [1] we can dynamically render React components from configuration rather than code. I.e.
const Form = JSONSchemaForm.default;
const schema = {
title: "Test form",
type: "object",
properties: {
name: {
type: "string"
},
age: {
type: "number"
}
}
};
ReactDOM.render((
<Form schema={schema} />
), document.getElementById("app"));
And we can use uiSchema object to customize and configure the individual look of each component. Is it possible however, instead of having the form created in a linear fashion, can I obtain each individual element and position them where I want?
For example, I may want the name field on page1 and the age field on page3. The only way I can think to do this currently is editing the package's source to return individual elements rather than wrapping them into the Form object.
Maybe there is some way in React to interrogate and extract the individual components from the Form object?
Thanks
[1] https://react-jsonschema-form.readthedocs.io/en/latest/

No, there isn't a way to get rjsf to split a form into multiple UI components. You can emulate that behavior by making multiple forms (multiple JSON Schema schemas). Depending on the structure of the combined output you want, it may be simplest to break the schema down by different nested property apparent at the root level, e.g. the 1st page corresponds to the 1st key at the root level (name), the 2nd page corresponds to the 2nd key (age), and so on. Note that you'd have to control this pagination and navigation yourself, and also merge each page's formData back into the singular payload you were expecting.
Here's a rough draft:
pageOneSchema = {
title: "Test form pg. 1",
type: "object",
properties: {
name: {
type: "string"
}
}
};
pageTwoSchema = {
title: "Test form pg. 2",
type: "object",
properties: {
age: {
type: "number"
}
}
};
const schemas = [pageOneSchema, pageTwoSchema];
// Your component's member functions to orchestrate the form and its "pages"
state = {
currentPage: 0,
pages: new Array(schemas.length).fill({}), // initial pages array to empty objects = unfilled forms
}
pageDataChangeHandler = (page) => {
const onChange = (e) => {
const pages = [...this.state.pages];
pages[page] = e.formData; // not too 100% about the key
this.setState({
pages,
});
}
}
getSubmitHandlerForPage = (page) => {
if (page !== schemas.length) {
// If current page is not the last page, move to next page
this.setState({
currentPage: page + 1,
});
} else {
// concat all the pages' data together to recompose the singular payload
}
}
...
const { currentPage } = this.state;
<Form
formData={this.state.pages[currentPage]}
schema={schemas[currentPage]}
onChange={this.pageDataChangeHandler(currentPage)}
onSubmit={this.getSubmitHandlerForPage(currentPage)}
/>
You can also pass children into the Form component to render your own buttons (so maybe you don't want the forms saying "Submit" instead of "Next Page" if they're not actually going to submit).
Note that if you do custom buttons and make a "Next" button, it must still be of "submit" type because of the type of event it emits (used for validation and some other things). Though there was an ask to make the text/title of the submit button easier to manipulate...

Related

How can I check all checkboxes with one checkbox and add a class to their parent element in React using use-state?

I am trying to do a to-do app and at one point I am completely stuck.
This is my current page so far:
I am going to explain to you what I am trying to do on the above page in short.
I have text input in which I write my to-dos.
When I submit it, it appears below in a li element. There is an array and those strings are stored there and printed with map method.
There are circle checkboxes left of the writings and when I checked them a class is added to a li element which is "completed"
Till here I have had no problem. Now there is a down arrow left of text input. When I checked it I wanted it to check all checkboxes down below and add "completed" class to their li elements. It is coded like:
<input className = "toggle-all" type = "checkbox"/>
<label htmlFor = "toggle-all" >
Mark all as complete
</label>
And here is my App.js, two components. Footer is not important.
Main component, Footer is not important again.
Header component
And the component I am struggling with is the Section
I have tried a lot of things yet I couldn't find a solution. Might be too many images and explanations but I wanted to make it clear for everybody. I want to use usestate to track all checkboxes but if the solution is not possible like that I am okay with the other solutions.
4.11.22
I created this question 2 days ago. During these two days, I tried various codes yet I couldn't make them work the way I wanted.
Instead of creating setTexts as array of string set it as array of object. Where object can be like this:
let obj = {
text: "todo item 1",
ischecked: false
}
const [listItem, setListItem] = useState([])
when adding new items in list you can do like this:
setListItem([...listItem, {text: "something to do another list", ischecked: false}])
when checking individual item call method onCheck(args):
const onCheck = (selectedItem) => {
let updatedList = listItem.map((item) => {
if(item.text === selectedItem.text){
return {
text: item.text,
ischecked: true
}
}
return item
})
setListItem([...updatedList])
}
And Finally to your questions answer. Once you click dropdown arrow call method checkedAllToDoList():
const checkedAlToDoList = () => {
let updatedList = listItem.map((item) => {
return {
text: item,
ischecked: true
}
})
setListItem([...updatedList])
}
Note: complete the flow as which method should pass as a props on
which components

Filtered list: how best to keep keyboard nav in sync with the list as rendered?

I'm building out keyboard navigation for a composable React combobox component. As it stands, it works except when the visible list of options is filtered down to match text field input. I'm using React.Children in the callback below to build an array corresponding to my options list; this works well on initialization, but after filtering (and re-rendering the options that match the filter) the result when I call it still includes all items, rendered or not, with the props they initially had. (I had the "bright idea" of giving unrendered children a tabindex of -2 and filtering that way, but this only works if the values are updated...)
Is there a better way than React.Children to populate my array of IDs and values/labels for my options, and to update it based on what was most recently rendered?
If so, where could I learn about it?
If not:
how can I access the current state, rather than a stale one?
what's the best way of pointing to the specific children in question? The method I'm using looks brittle.
const updateOptions = useCallback(
() =>
// set the optionStateMap from children
React.Children.forEach(children, child => {
if (!React.isValidElement(child)) return;
if (!child.props.children) return;
const optionsArray: Array<optionStateProps> = [];
child.props.children[0].forEach( // will break if my list is not the first child with children; d'oh
(option: { props: { id: string; label: string, tabIndex: number} }) => {
if (option.props.tabIndex < -1) return;
optionsArray.push({
id: option.props.id,
label: option.props.label
});
}
);
setOptionStateMap(optionsArray);
}),
[children]
);
Thanks for looking!

Handling children state in an arbitrary JSON tree

This is more of a brainstorming question as I can't really seem to come up with a good solution.
I have a component which renders a tree based on some passed JSON (stored at the top level). Each node of the tree can have 0..n children and maps to a component defined by the JSON of that node (can be basically anything is the idea). The following is just an example and the names don't mean anything specific. Don't pay too much attention to the names and why a UserList might have children that could be anything.
JSON: {
data: {}
children: [
{
data: {}
children: []
},
{
data: {}
children: []
},
{
data: {}
children: [
{
data: {}
children: []
},
...etc
]
},
]
}
const findComponent = (props) => {
if (props.data.name === "userSelector") {
return <UserSelectorNode {...props}>;
} else if (props.data.name === "userInformation") {
return <UserInformationNode{...props}>; // example of what might be under a userSelectorNode
}
...etc
};
// render a user selector and children
const UserSelectorNode = (props) => {
const [selected, setSelected] = React.useState([])
// other methods which can update the JSON when selected changes...
return (
<div>
<UserSelector selected={selected}/> // does a getUser() server request internally
<div>
{props.data.children.map((child) => findComponent(child))}
<div>
</div>
);
};
This tree can be modified at any level (add/remove/edit). Adding/Editing is easy. The problem is remove operations.
Some children components use existing components which do things like getting a list of users and displaying them in a list (stored in state I have no access to). When a node on the tree is removed new components are made for every node that has to shift (JSON at the index is now different), which can be a lot. This causes a bunch of requests to occur again and sometimes state can be lost entirely (say the page number of a table to view users).
I'm pretty sure there is no way for the "new" UserSelector created when the JSON shifts to keep the same state, but I figured I may as well ask if anyone has dealt with anything similar and how they went about designing it.
The only way I can think of is to not actually reuse any components and re implement them with state stored somewhere else (which would suck), or rewrite everything to be able to take internal state as well as an external state storage if required.
EDIT: Added Sandbox
https://codesandbox.io/s/focused-thunder-neyxf. Threw it together pretty quick to only get a single layer of remove working which shows the problem.

React Parent-Child Relationship and Encapsulation

I have a React design problem that I am trying to solve, and I am running into an issue counter-intuitive to the idea of encapsulation as I try to breakdown an existing component into parent-child to support an additional use case. I am using React 15.3.2 with Redux, ES6, and ImmutableJS. First, I will illustrate the design problem, then I will provide snippets to illustrate why I feel that I have the need to get data back from children and how that is good for encapsulation, and the roadblock I am hitting.
I have read this stackoverflow which has detailed explanation on why passing data from children to parent component is not a good idea,
Pass props to parent component in React.js
But I have some concerns, which I will discuss at the end.
Design:
I am starting with a CheckboxSelect component. The Title bar's text depends on the checked items.
Closed:
Open with selections (current implementation):
To support additional use-case, the dropdown will now open up with more stuff.
Open with selections (new update):
Initial Code:
I am starting with a CheckboxSelect controlled component with the following props interface:
CheckboxSelect.jsx:
CheckboxSelect.propTypes = {
// what title to display by default with no selection
defaultTitle: PropTypes.string.isRequired, ie. "Selected Scopes"
// array of selected options, ie. [{key: "comedy", name: "comedy", checked: false }, ...]
options: PropTypes.array.isRequired,
// handler for when the user checks a selection, this will update
// the reducer state, which causes the options prop to be refreshed and
// passed in from the outer container
onCheck: PropTypes.func.isRequired,
onUncheck: PropTypes.func.isRequired,
onCheckAll: PropTypes.func,
onUncheckAll: PropTypes.func,
className: PropTypes.string,
// controls the open state of the dropdown
open: PropTypes.bool,
// handler for when the user clicks on the dropdown button, this will update the reducer state,
// which causes the open prop to be refreshed and passed in from the outer container
onClick: PropTypes.func,
onCancel: PropTypes.func,
};
// there is currently some logic inside the component to generate the title to display
// produces "comedy, action"
getTitle() {
const { defaultTitle } = this.props;
const checked = this.getChecked();
let fullTitle;
if (this.allChecked() || this.allUnchecked()) {
fullTitle = `${defaultTitle } (${checked.length})`;
} else {
fullTitle = checked.map((option) => option.name).join(', ');
}
return fullTitle;
}
getChecked() {
const { options } = this.props;
return options.filter(option => option.checked);
}
allChecked() {
const { options } = this.props;
return this.getChecked().length === options.length;
}
allUnchecked() {
return this.getChecked().length === 0;
}
ApplicationContainer.jsx (where the component is being used):
scopeDropDownOptions = (currentMovie) => {
// returns [{key: "comedy", name: "comedy", checked: false }]
const applicableScopes = currentMovie.getIn(['attributes', 'applicable_scopes']);
return currentMovie.getIn(['attributes', 'available_scopes']).reduce((result, scope) => {
result.push({
key: scope,
name: scope,
checked: (applicableScopes.includes(scope)),
});
return result;
}, []);
}
onSelectScope = (scope) => {
const movieScopes = this.applicableScopes.push(scope.key);
this.onUpdateField('applicable_scopes', movieScopes);
}
render() {
...
<CheckboxSelect
defaultTitle="Selected Scopes"
options={this.scopeDropdownOptions(currentMovie)}
onCheck={this.onSelectScope}
onUncheck={this.onDeselectScope}
onCheckAll={this.onSelectAllScopes}
onUncheckAll={this.onDeselectAllScopes}
open={store.get('scopeDropdownOpen')}
</CheckboxSelect>
}
New Code:
To support the new layout, I would like to break the existing component into two: a DynamicDropdown that contains CheckboxSelect2 as one of the children, along with any other elements that may be dropped down. Here is how the new code will look inside the ApplicationContainer.
ApplicationContainer.jsx
scopeDropDownOptions = (currentMovie) => {
// returns [{key: "comedy", name: "comedy", checked: false }]
const applicableScopes = currentMovie.getIn(['attributes', 'applicable_scopes']);
return currentMovie.getIn(['attributes', 'available_scopes']).reduce((result, scope) => {
result.push({
key: scope,
name: scope,
checked: (applicableScopes.includes(scope)),
});
return result;
}, []);
}
onSelectScope = (scope) => {
const {store } = this.props;
const cachedApplicableScopes = store.get('cachedApplicableScopes').push(scope.key);
store.get('cachedApplicableScopes').push(scope.key);
this.actions.setCachedApplicableScopes(cachedApplicableScopes);
// wait until apply is clicked before update
}
render() {
...
<DynamicDropdown
className="scope-select"
title={this.scopeDropdownTitle()}
open={store.get('scopeDropdownOpen')}
onClick={this.onScopeDropdownClick}
onCancel={this.onScopeDropdownCancel}
>
<CheckboxSelect2
options={this.scopeDropdownOptions(currentMovie)}
onCheck={this.onSelectScope}
onUncheck={this.onDeselectScope}
onCheckAll={this.onSelectAllScopes}
onUncheckAll={this.onDeselectAllScopes}
visble={store.get('scopeDropdownOpen')}
/>
// ... other children like confirmation message and buttons
</DynamicDropdown>
}
// logic related to CheckboxSelect2 title generation moved to the ApplicationContainer. Not ideal in my opinion as it breaks encapsulation. Further discussions in the Choices section
getScopesChecked() {
const options = this.scopeDropdownOptions(this.currentMovie);
return options.filter(option => option.checked);
}
scopesAllChecked() {
const options = this.scopeDropdownOptions(this.currentMovie);
return this.getScopesChecked().length === options.length;
}
scopesAllUnchecked() {
return this.getScopesChecked().length === 0;
}
scopeDropdownTitle() {
const defaultTitle = "Selected Scopes";
const checked = this.getScopesChecked();
let fullTitle;
if (this.scopesAllChecked() || this.scopesAllUnchecked()) {
fullTitle = `${defaultTitle} (${checked.length})`;
} else {
fullTitle = checked.map((option) => option.name).join(', ');
}
return fullTitle;
}
Problem:
The problem I have is with populating the title props of the DynamicDropdown element with the New Code, since it depends on the result of the CheckboxSelect2 selection.
Keep in mind CheckboxSelect2 is a dumb controlled component, we pass an onCheck handler to it. The this.onSelectScope inside the ApplicationContainer, is responsible for updating the reducer state of what has been checked, which in turn refresh the props and causes the DynamicDropdown and CheckboxSelect2 to be re-rendered.
Choices:
In the old code, there is a group of logic used to figure out the title to display for the dropdown. Here are the choices I am presented with:
To keep encapsulation of letting the CheckboxSelect2 summarize the
title, I tried initially keeping the same title logic inside
CheckboxSelect2, and accessing it via a ref.
ApplicationContainer.jsx
<DynamicDropdown
title={this.childCheckboxSelect.getTitle()}
>
<CheckboxSelect2
ref={(childCheckboxSelect) => this.childCheckboxSelect = childCheckboxSelect}
>
</DynamicDropdown>
At the time that DynamicDropdown is re-rendered, CheckboxSelect2
hasn't received the new props yet from the parent via the one-way
data-flow, so as a child, it actually cannot generate the most
up-to-date title for the DynamicDropdown based on what has been
checked. With this implementation, my title is one-step behind what
was actually checked.
As shown in the ApplicationContainer.jsx for the New Code section
above, the logic for the scopeDropdownTitle could be moved out from
CheckboxSelect2 to ApplicationContainer.jsx, so it sits a level
above DynamicDropdown, enabling the DynamicDropdown to get the
updated title from the reducer state as it renders. While this
solution works, it totally breaks my view on encapsulation, where
the responsibility for determining what title to be displayed, is
something that the CheckboxSelect2 should know about. Basically the
title logic in ApplicationContainer.jsx, now also pre-generates the
options props meant to passed to CheckboxSelect2 to render that
component, the CheckboxSelect2 logic is bleeding into the outer
container.
Let's look at the argument in the stackoverflow post Pass props to parent component in React.js and how it relates to this design problem and an analogy:
"The parent already has that child prop. ...  if the child has a
prop, then it is because its parent provided that prop to the
child!"  Sure, the ApplicationContainer has all the knowledge it
needs to generate the title for the parent DynamicDropdown based on
the checked states of the child CheckboxSelect2, but then is it
really the responsibility of the ApplicationContainer?
Let me give an analogy of a Manager asking an Employee to produce a Report. You can say, the Manager already has all the info, he can surely produce the Report himself! Having a controlled component where the
container manages update to the props for the child via callbacks, seems to me like a Manager passing a bucket to the Employee, the Employee passes back bits and pieces of information in the bucket, and tell the Manager to do the work to summarize things instead of the Employee producing a good summary (which is good encapsulation).
"Additionally, you could have used a ref directly on the child"
I think in the Choices 1) I stated above, using ref does not seem to work when you want up-to-date information from the child as
there is a circular dependency issue with one-way dataflow (ie.
parent needs to get up-to-date summary information from the child,
but the child first depends on the up-to-date information from the
parent).
If you have read this end-to-end and understood the problem, I appreciate your effort! Let me know what you think.

A sensible way to design a reactjs page components with a large parent data structure

I've just started on reactjs and am trying to figure out the right way to design my components. I get the concept of components, props, states but struggling a bit with the right way to design for a hierarchy.
I have a page that's driven by a big object array. Say
objArr = {
param1 : value1,
param2: value2,
dataArray: [
{ key: "keyvalue", a: "a", b: "b", c: "c" },
...
]
}
The entire page builds off of this. The page builds a series of UI components corresponding to the dataArray.
Now each time some of the icons are clicked in the UI, I want some changes, and the icons correspond to a value on this dataArray. What's a good way to ensure the dataArray values are changed as the UI is acted on? and vice versa, the UI changes as values are changed on the dataArray.
I've read "make components as stateless as possible," fine - then how do I do this handling at the parent component level and have it flow down?
I don't need code examples. Just a few pointers of the way to architect my ReactJS code would be great, I will figure out the code.
Thank you
What you can do is bind a method on the Parent component and pass it down to the child container like so:
// Our 'pages' that we want to be able to interact with
const ComponentWIthButtons = ({handleButtonClick, buttons}) => (
<div>
<p>Hey, click one of these</p>
{buttons.map(button => (
<div onClick={()=>handleButtonClick(button.name)}>
{button.text}
</div>
))}
</div>
)
// The Parent we want to interact with
class App extends React.Component {
constructor(props){
super(props)
this.state = {
buttons: [
{
name: 'a',
text: 'Awesome button'
},
{
name: 'b',
text: 'Beautiful button'
},
{
name: 'c',
text: 'Cool button'
}
]
}
}
// This is what you want to do with the information passed
handleButtonClick = (name) => {
// this could be updating this components' state, which
// would rerender the display component because we pass it
// our buttons from our state object
console.log(name)
};
render(){
return <div>
<h2>Below are some buttons!</h2>
// If you don't use the syntax above, you will run into
// errors trying to get a reference to this container through
// `this`. You can do this.handleButtonClick.bind(this)
// to get around that
<ComponentWIthButtons handleButtonClick={this.handleButtonClick} buttons={this.state.buttons} />
</div>
}
}

Resources