For some reason, its not actually accessing the exercises method. Can someone tell me what I'm doing wrong here. Appreciate it!
class StartWorkout extends Component {
state = {
step: 0
};
exercises = () => {
const { exerciselist } = this.props.selectedWorkout;
const { step } = this.state.step;
while (step < exerciselist.length) {
return <StartExercise exercise={exerciselist[step]} />;
}
};
render() {
const { name } = this.props.selectedWorkout;
const { step } = this.state;
return (
<>
<ClientMenuBar title={name} />
<Container maxWidth="xl">
{this.exercises()}
<div style={styles.buttonWrapper}>
<Button
color="inherit"
variant="contained"
style={styles.button}
size="large"
>
Back
</Button>
<Button
color="primary"
variant="contained"
type="submit"
style={styles.button}
size="large"
onClick={() => {
this.setState({ step: step + 1 });
}}
>
Next
</Button>
</div>
</Container>
</>
);
}
}
I know its not actually going into the method because I am console logging right before i access it and right before the while loop in the function and its only calling the first.
The issue is with the exercises method. Here you are trying to accessstep from this.state.step. Replace const { step } = this.state.step; . by const { step } = this.state;.
Notice the extra step property which you are trying to access which comes as undefined and undefined < any number is false, and hence the code inside while loop never gets executed.
A few problems here:
// this line is double-dereferencing 'step'
const { step } = this.state.step;
// step will be undefined here
while (step < exerciselist.length) {
// if you're going to return this, what's the loop for?
// (and if you don't return, this loop will never exit
// because 'step' never changes)
return <StartExercise exercise={exerciselist[step]} />;
}
Here's an updated example of how you might go about it now that I better understand what you're trying to do.
class Exercises extends React.Component {
state = {step: 0}
render() {
const {step} = this.state;
const {workout: {exercises}} = this.props;
return (
<div>
<div> {exercises[step]} </div>
<button
disabled={step < 1}
onClick = {() => this.setState({step: step - 1})}
>
Previous
</button>
<button
disabled={step >= exercises.length - 1}
onClick = {() => this.setState({step: step + 1})}
>
Next
</button>
</div>
)
}
}
const workout = {
exercises: Array.from({length: 5}, (_, i) => `Exercise ${i + 1}`)
};
ReactDOM.render(
<Exercises workout={workout} />,
document.querySelector("#app")
);
body {
background: #20262E;
padding: 20px;
font-family: Helvetica;
}
#app {
background: #fff;
border-radius: 4px;
padding: 20px;
transition: all 0.2s;
}
li {
margin: 8px 0;
}
h2 {
font-weight: bold;
margin-bottom: 15px;
}
.done {
color: rgba(0, 0, 0, 0.3);
text-decoration: line-through;
}
input {
margin-right: 5px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<div id="app"></div>
You need to use constructor. Update your code like this:
class StartWorkout extends Component {
constructor(props){
super(props);
this.state = {
step: 0,
};
}
exercises = () => {
const { exerciselist } = this.props.selectedWorkout;
// The way your destructuring the state is wrong. It should be like this.
const { step } = this.state; // not required anymore
return exerciselist.map(item => {
return <StartExercise exercise={item} />;
}
};
// Use your render function as it is.
}
Hope this helps!!
Related
I am making a simple react application and included react-stepper-horizontal library and things are fine.
Working Example:
Appropriate Code related to stepper:
const Form = () => {
.
.
.
const [currentPage, setCurrentPage] = useState(1);
const sections = [
{ title: 'Basic Details', onClick: () => setCurrentPage(1) },
{ title: 'Employment Details', onClick: () => setCurrentPage(2) },
{ title: 'Review', onClick: () => setCurrentPage(3) },
];
<Stepper
steps={sections}
activeStep={currentPage}
activeColor="red"
defaultBarColor="red"
completeColor="green"
completeBarColor="green"
/>
.
.
.
}
Steps to reproduce issue:
-> There are totally three steps 1,2,3 and each have different sections as Basic Details, Employment Details and Review respectively.
-> Now if user enter any of the input field in Step 1 and goes to Step 2 and fill some input fields there and goes to Step 3 to review it and if he comes back to the Step 1 again then the active state is lost in Step 3.
-> So now issue is if we want to go to step 3 then we need to again go three steps to reach last Step 3.
Requirement:
-> If user once visited any step then if he comes to any previous step then all the steps that he visited previously needs to be in active.
Eg:
-> If user landed in Step 1, then using next button , he reaches the Step 3 and if he wish to come back to Step 1 to modify some inputs and again if he wants to go to Step 3 for review step then it should be possible by clicking on the Step 3 because he already visited that step.
Kindly help me to achieve the result of making the steps in active state upto which the user visits.. If user visits Step 3 and goes back to step 1 on click of the Step 1 circle then there should be possibility to come back to Step 3 again as he already visited the Step 3..
Any solution without any library also welcomed.
This is a big issue if we have more steps (eg 7 steps). So please kindly help me.. A big thanks in advance..
Here's a simple implementation of the <Stepper /> component in question. The key is to have a tracker that tracks the visited steps internally, persist that information across re-renders.
Demoboard Playground
const { useState, useEffect, useMemo } = React;
const cx = classNames;
function range(a, b) {
return new Array(Math.abs(a - b) + 1).fill(a).map((v, i) => v + i);
}
function Stepper({ steps, activeStep, children }) {
const count = steps.length;
const listOfNum = useMemo(() => range(1, count), [count]);
const tracker = useMemo(() => {
let highestStep = 0;
function hasVisited(step) {
return highestStep >= step;
}
function addToBackLog(step) {
if (step > highestStep) highestStep = step;
}
return {
hasVisited,
addToBackLog,
getHighestStep() {
return highestStep;
},
};
}, []);
tracker.addToBackLog(activeStep);
const noop = () => {};
const prevStep = steps[activeStep - 2];
const currentStep = steps[activeStep - 1];
const nextStep = steps[activeStep];
return (
<div>
<div>
{" "}
{listOfNum.map((num, i) => {
const isActive = activeStep == num;
const isVisited = tracker.hasVisited(num);
const isClickable = num <= tracker.getHighestStep() + 1 || isVisited;
return (
<div
key={num}
className={cx("circle", {
active: isActive,
visited: isVisited,
clickable: isClickable,
})}
onClick={isClickable ? steps[i].onClick : noop}
>
{num}{" "}
</div>
);
})}{" "}
</div>{" "}
<h2> {currentStep && currentStep.title} </h2> <div> {children} </div>{" "}
<div className="footer">
{" "}
{prevStep ? (
<button onClick={prevStep.onClick}> prev </button>
) : null}{" "}
{nextStep ? <button onClick={nextStep.onClick}> next </button> : null}{" "}
</div>{" "}
</div>
);
}
function App() {
const [currentPage, setCurrentPage] = useState(1);
const sections = [
{
title: "Un",
onClick: () => setCurrentPage(1),
},
{
title: "Deux",
onClick: () => setCurrentPage(2),
},
{
title: "Trois",
onClick: () => setCurrentPage(3),
},
{
title: "Quatre",
onClick: () => setCurrentPage(4),
},
{
title: "Cinq",
onClick: () => setCurrentPage(5),
},
];
return (
<Stepper steps={sections} activeStep={currentPage}>
I 'm page {currentPage}{" "}
</Stepper>
);
}
ReactDOM.render(<App />, document.getElementById("root"));
body {
color: #0f0035;
padding-bottom: 2rem;
}
.circle {
display: inline-flex;
height: 2em;
width: 2em;
align-items: center;
justify-content: center;
border-radius: 50%;
background-color: lightgrey;
margin: 0 0.5em;
color: white;
cursor: not-allowed;
}
.active {
background-color: rgba(50, 50, 250) !important;
}
.visited {
background-color: rgba(50, 50, 250, 0.5);
}
.clickable {
cursor: pointer;
}
.footer {
margin-top: 1em;
display: flex;
justify-content: space-between;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/classnames/2.2.6/index.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.0/umd/react-dom.production.min.js"></script>
<div id="root"></div>
I try to build a to-do-list in react.
I have 2 components so far:
The first one handles the input:
import React from 'react';
import ListItems from './ListItems.js';
class InputComponent extends React.Component {
constructor(){
super();
this.state = {
entries: []
}
this.getText = this.getText.bind(this);
}
getText() {
if(this._inputField.value !== '') {
let newItem = {
text: this._inputField.value,
index: Date.now()
}
this.setState((prevState) => {
return {
entries: prevState.entries.concat(newItem)
}
})
this._inputField.value = '';
this._inputField.focus();
}
}
render() {
return(
<div>
<input ref={ (r) => this._inputField = r } >
</input>
<button onClick={ () => this.getText() }>Go</button>
<div>
<ListItems
entries={this.state.entries}
/>
</div>
</div>
)
}
}
export default InputComponent;
The second one is about the actual entries in the list:
import React from 'react';
class ListItems extends React.Component {
constructor() {
super();
this.lineThrough = this.lineThrough.bind(this);
this.listTasks = this.listTasks.bind(this);
}
lineThrough(item) {
console.log(item);
//item.style = {
// textDecoration: 'line-through'
//}
}
listTasks(item) {
return(
<li key = { item.index }>
<div
ref = { (r) => this._itemText = r }
style = {{
width: 50 + '%',
display: 'inline-block',
backgroundColor: 'teal',
color: 'white',
padding: 10 + 'px',
margin: 5 + 'px',
borderRadius: 5 + 'px'
}}
>
{ item.text }
</div>
<button onClick={ () => this.lineThrough(this._itemText) }>Done!</button>
<button>Dismiss!</button>
</li>
)
}
render() {
let items = this.props.entries;
let listThem = items.map( this.listTasks );
return(
<ul style = {{
listStyle: 'none'
}}>
<div>
{ listThem }
</div>
</ul>
)
}
}
export default ListItems;
As you can see, i want to have two buttons for each entry, one for the text to be line-through, and one to delete the entry.
I am currently stuck at the point where i try to address a specific entry with the "Done!" button to line-through this entry's text.
I set a ref on the div containing the text i want to style and pass that ref to the onClick event handler.
Anyways, the ref seems to be overwritten each time i post a new entry...
Now, always the last of all entries is addressed. How can i properly address each one of the entries?
What would be the best practice to solve such a problem?
you could pass an additional prop with index/key of the todo into every item of your todo list. With passing event object to your handler lineThrough() you can now get the related todo id from the attributes of your event target.
Kind regards
class TodoList extends Component {
constructor(props) {
super(props);
this.state = {
todos: []
}
this.done = this.done.bind(this);
}
done(id) {
this.state.todos[id].done();
}
render() {
return (
this.state.todos.map(t => <Todo item={t} onDone={this.done} />)
);
}
}
const Todo = ({item, onDone}) => {
return (
<div>
<h1>{item.title}</h1>
<button onClick={() => onDone(item.id)}>done</button>
</div>
)
}
You need map listItems then every list item get its own ref
import React from 'react';
import ReactDOM from 'react-dom';
class ListItems extends React.Component {
constructor() {
super();
this.lineThrough = this.lineThrough.bind(this);
}
lineThrough(item) {
item.style.textDecoration = "line-through";
}
render() {
return(
<ul style = {{
listStyle: 'none'
}}>
<div>
<li key={this.props.item.index}>
<div
ref={(r) => this._itemText = r}
style={{
width: 50 + '%',
display: 'inline-block',
backgroundColor: 'teal',
color: 'white',
padding: 10 + 'px',
margin: 5 + 'px',
borderRadius: 5 + 'px'
}}
>
{this.props.item.text}
</div>
<button onClick={() => this.lineThrough(this._itemText)}>Done!</button>
<button>Dismiss!</button>
</li>
</div>
</ul>
)
}
}
class InputComponent extends React.Component {
constructor(){
super();
this.state = {
entries: []
}
this.getText = this.getText.bind(this);
}
getText() {
if(this._inputField.value !== '') {
let newItem = {
text: this._inputField.value,
index: Date.now()
}
this.setState((prevState) => {
return {
entries: prevState.entries.concat(newItem)
}
})
this._inputField.value = '';
this._inputField.focus();
}
}
render() {
return(
<div>
<input ref={ (r) => this._inputField = r } >
</input>
<button onClick={ () => this.getText() }>Go</button>
<div>
{this.state.entries.map((item, index) => {
return <ListItems key={index} item={item} />
})}
</div>
</div>
)
}
}
Every time I click an option of size and click add to cart I would like to add the data of the selected object to this array cart. This currently works kinda but only one object can be added and when you try to do it again the old data disappears and is replaced with the new object.
I would like to keep odd objects in the array and add new objects too. How do I go about doing this?
index.js
export class App extends Component {
constructor(props) {
super(props);
this.state = {
evenSelected: null
};
}
handleSelectL1 = i => {
this.setState({
evenSelected: i,
oldSelected: null
});
};
render() {
const product = [
{
name: " size one",
price: 1
},
{
name: "size two",
price: 2
},
,
{
name: "size three",
price: 3
}
];
const cart = [];
const addCart = function() {
cart.push(product[evenIndex]);
if (cart.length > 0) {
}
};
console.log("cart", cart);
const evenIndex = this.state.evenSelected;
const priceShown = product[evenIndex] && product[evenIndex].price;
return (
<div>
<Child
product={product}
handleSelectL1={this.handleSelectL1}
evenIndex={evenIndex}
/>
<h2>Price:{priceShown} </h2>
<button onClick={addCart}>Add to cart</button>
</div>
);
}
}
child.js
export class Child extends Component {
constructor(props) {
super(props);
this.state = {};
}
render() {
const { product, evenIndex } = this.props;
return (
<div>
{product.map((p, i) => {
return (
<div
key={p.id}
className={evenIndex === i ? "selectedRBox" : "selectorRBox"}
onClick={() => this.props.handleSelectL1(i)}
>
<h1 className="selectorTextL">{p.name}</h1>
</div>
);
})}
</div>
);
}
}
Here is my code on sandbox: https://codesandbox.io/s/14vyy31nlj
I've just modified your code to make it work. Here is the complete code. You need cart to be part of the state, so it does not initialize in each render, and to make the component render again when you add an element.
Remove the function to make it a method of the class:
addToCart() {
const selectedProduct = products[this.state.evenSelected];
this.setState({
cart: [...this.state.cart, selectedProduct]
});
}
And call it on render:
render() {
console.log("cart", this.state.cart);
const evenIndex = this.state.evenSelected;
const priceShown = products[evenIndex] && products[evenIndex].price;
return (
<div>
<Child
product={products}
handleSelectL1={this.handleSelectL1}
evenIndex={evenIndex}
/>
<h2>Price:{priceShown} </h2>
<button onClick={this.addToCart.bind(this)}>Add to cart</button>
</div>
);
}
}
Check that I have binded on render, which can bring performance issues in some cases. You should check this
Update
As devserkan made me notice (Thanks!), when you use the previous state to define the new state (for example adding an element to an array), it is better to use the updater function instead of passing the new object to merge:
this.setState(prevState => ({
cart: [...prevState.cart, products[selectedProduct]],
}));
For more info check the official docs.
I don't quite understand what are you trying to but with a little change here it is. I've moved product out of the components like a static variable. Also, I've changed the addCart method, set the state there without mutating the original one and keeping the old objects.
const product = [
{
name: " size one",
price: 1
},
{
name: "size two",
price: 2
},
{
name: "size three",
price: 3
}
];
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
evenSelected: null,
cart: [],
};
}
handleSelectL1 = i => {
this.setState({
evenSelected: i,
oldSelected: null
});
};
addCart = () => {
const evenIndex = this.state.evenSelected;
this.setState( prevState => ({
cart: [ ...prevState.cart, product[evenIndex] ],
}))
};
render() {
console.log(this.state.cart);
const evenIndex = this.state.evenSelected;
const priceShown = product[evenIndex] && product[evenIndex].price;
return (
<div>
<Child
product={product}
handleSelectL1={this.handleSelectL1}
evenIndex={evenIndex}
/>
<h2>Price:{priceShown} </h2>
<button onClick={this.addCart}>Add to cart</button>
</div>
);
}
}
class Child extends React.Component {
constructor(props) {
super(props);
this.state = {};
}
render() {
const { product, evenIndex } = this.props;
return (
<div>
{product.map((p, i) => {
return (
<div
key={p.id}
className={evenIndex === i ? "selectedRBox" : "selectorRBox"}
onClick={() => this.props.handleSelectL1(i)}
>
<h1 className="selectorTextL">{p.name}</h1>
</div>
);
})}
</div>
);
}
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
.selectorRBox {
width: 260px;
height: 29.5px;
border: 1px solid #727272;
margin-top: 18px;
}
.selectedRBox {
width: 254px;
height: 29.5px;
margin-top: 14px;
border: 4px solid pink;
}
.selectorTextL {
font-family: "Shree Devanagari 714";
color: #727272;
cursor: pointer;
font-size: 18px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id="root"></div>
In Draft-JS, I would like a basic custom block, rendering an <h1> element. I would like to add some text before my h1, that the user cannot edit. The text is here to inform people that this block is for Title. So I would like to add "TITLE" in front of the block that is not editable.
What is the best way to achieve this in Draft JS?
You can achieve your aim by applying contentEditable={false} and readOnly property on the node that should be read-only:
class MyCustomBlock extends React.Component {
constructor(props) {
super(props);
}
render() {
return (
<div className="my-custom-block">
<h1
contentEditable={false} // <== !!!
readOnly // <== !!!
>
Not editable title
</h1>
<div className="editable-area">
<EditorBlock {...this.props} />
</div>
</div>
);
}
}
Check working demo in the hidden snippet below:
const {Editor, CharacterMetadata, DefaultDraftBlockRenderMap, ContentBlock, EditorBlock, genKey, ContentState, EditorState} = Draft;
const { List, Map, Repeat } = Immutable;
class MyCustomBlock extends React.Component {
constructor(props) {
super(props);
}
render() {
return (
<div className="my-custom-block">
<h1
contentEditable={false}
readOnly
>
Not editable title
</h1>
<div className="editable-area">
<EditorBlock {...this.props} />
</div>
</div>
);
}
}
function blockRendererFn(contentBlock) {
const type = contentBlock.getType();
if (type === 'MyCustomBlock') {
return {
component: MyCustomBlock,
props: {}
};
}
}
const RenderMap = new Map({
MyCustomBlock: {
element: 'div',
}
}).merge(DefaultDraftBlockRenderMap);
const extendedBlockRenderMap = Draft.DefaultDraftBlockRenderMap.merge(RenderMap);
class Container extends React.Component {
constructor(props) {
super(props);
this.state = {
editorState: EditorState.createEmpty()
};
}
_handleChange = (editorState) => {
this.setState({ editorState });
}
_onAddCustomBlock = () => {
const selection = this.state.editorState.getSelection();
this._handleChange(addNewBlockAt(
this.state.editorState,
selection.getAnchorKey(),
'MyCustomBlock'
))
}
render() {
return (
<div>
<div className="container-root">
<Editor
placeholder="Type"
blockRenderMap={extendedBlockRenderMap}
blockRendererFn={blockRendererFn}
editorState={this.state.editorState}
onChange={this._handleChange}
/>
</div>
<button onClick={this._onAddCustomBlock}>
ADD CUSTOM BLOCK
</button>
</div>
);
}
}
ReactDOM.render(<Container />, document.getElementById('react-root'));
const addNewBlockAt = (
editorState,
pivotBlockKey,
newBlockType = 'unstyled',
initialData = new Map({})
) => {
const content = editorState.getCurrentContent();
const blockMap = content.getBlockMap();
const block = blockMap.get(pivotBlockKey);
if (!block) {
throw new Error(`The pivot key - ${ pivotBlockKey } is not present in blockMap.`);
}
const blocksBefore = blockMap.toSeq().takeUntil((v) => (v === block));
const blocksAfter = blockMap.toSeq().skipUntil((v) => (v === block)).rest();
const newBlockKey = genKey();
const newBlock = new ContentBlock({
key: newBlockKey,
type: newBlockType,
text: '',
characterList: new List(),
depth: 0,
data: initialData,
});
const newBlockMap = blocksBefore.concat(
[[pivotBlockKey, block], [newBlockKey, newBlock]],
blocksAfter
).toOrderedMap();
const selection = editorState.getSelection();
const newContent = content.merge({
blockMap: newBlockMap,
selectionBefore: selection,
selectionAfter: selection.merge({
anchorKey: newBlockKey,
anchorOffset: 0,
focusKey: newBlockKey,
focusOffset: 0,
isBackward: false,
}),
});
return EditorState.push(editorState, newContent, 'split-block');
};
body {
font-family: Helvetica, sans-serif;
}
.container-root {
border: 1px solid black;
padding: 5px;
margin: 5px;
}
.my-custom-block {
background-color: cadetblue;
margin: 15px 0;
font-size: 16px;
position: relative;
}
.editable-area {
background-color: lightblue;
height: 50px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/immutable/3.8.1/immutable.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.3.0/react.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.3.0/react-dom.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/draft-js/0.10.0/Draft.js"></script>
<link href="https://cdnjs.cloudflare.com/ajax/libs/draft-js/0.7.0/Draft.css" rel="stylesheet"/>
<div id="react-root"></div>
If I have say the following simple components:
const CompOne = (props) => {
return (
<div className="compOne">
{props.children}
</div>
);
};
const CompTwo = (props) => {
return (
<div className="compTwo">
{props.children}
</div>
);
};
const CompThree = (props) => {
return (
<div className="compThree">
{props.content}
</div>
);
};
Now during run time, after making an AJAX request the client receives information that gives the order in which components need to wrap into one another. The result of that AJAX request would look something like this:
let renderMap = [
{ component: CompOne, props: {} },
{ component: CompTwo, props: {} },
{ component: CompThree, props: { content: "hi there" } }
];
So the composition should flow by iterating through the array and composing one component into the next. e.g: CompOne(CompTwo(CompThree)))
Two important things to note when I tried creating a wrapping HOC to fix this issue:
Edit: Important detail I forgot to mention in the original post
1) The number of components to wrap will not be consistent. At times it could be 3, but other times as many as 4, 5, 6 components needed to wrap into each other
2) The order could be different each time
<CompOne>
<CompTwo>
<CompThree content="hi there">
</CompThree>
</CompTwo>
</CompOne>
So my resulting HTML would be:
<div className="compOne">
<div className="compTwo">
<div className="compThree">
hi there
</div>
</div>
</div>
I've tried various things but I can't get it to work once I start getting past just wrapping two components. Is this something I can even do in React?
Like the link that Arup Rakshit posted in the comments showed, you can use components that are stored in a variable - with JSX - as long as they are capitalized:
// in render()
const MyComp = props.someVariableContainingAComponent;
return <MyComp />;
With that in mind, one approach to your problem would be to iterate through all your components, starting with inner one, and then taking the each of the next to use as a wrapper for the previous one. Given the shape of your test data renderMap, and using Array.protype.reduce for the iteration, it could look something like this:
renderComponents(renderMap) {
const Component = renderMap
.reverse()
.reduce( (ComponentSoFar, {component, props}) => {
const Outer = component;
return () => (<Outer {...props} ><ComponentSoFar /></Outer>);
}, props => null ); // initial value, just a "blank" component
return ( <Component /> );
}
I have included a demo showing how both different number of components and varying order of nesting can be handled with this approach.
const CompOne = (props) => (
<div className="comp compOne"><p>One:</p>{ props.content || props.children } </div>);
const CompTwo = (props) => (
<div className="comp compTwo"><p>Two:</p> { props.content || props.children }</div>);
const CompThree = (props) => (
<div className="comp compThree"><p>Three:</p> { props.content || props.children }</div>);
const CompFour = (props) => (
<div className="comp compFour"><p>Four:</p> { props.content || props.children }</div>);
const CompFive = (props) => (
<div className="comp compFive"><p>Five:</p> { props.content || props.children }</div>);
const renderMap1 = [
{ component: CompOne, props: {} },
{ component: CompTwo, props: {} },
{ component: CompThree, props: {} },
{ component: CompFour, props: {} },
{ component: CompFive, props: { content: "hi there" } }
];
const renderMap2 = [].concat(renderMap1.slice(1,4).reverse(), renderMap1.slice(4))
const renderMap3 = renderMap2.slice(1);
class App extends React.Component {
constructor(props) {
super(props);
}
renderComponents(renderMap) {
const Component = renderMap
.reverse()
.reduce( (ComponentSoFar, {component, props}) => {
const Outer = component;
return () => (<Outer {...props} ><ComponentSoFar /></Outer>);
}, props => null ); // initial value, just a "blank" component
return ( <Component /> );
}
render() {
return ( <div>
{ this.renderComponents(renderMap1) }
{ this.renderComponents(renderMap2) }
{ this.renderComponents(renderMap3) }
</div> );
}
};
ReactDOM.render(<App />, document.getElementById('root'));
.comp {
border: 5px solid green;
padding: 5px;
margin: 5px;
display: inline-block;
}
.compOne { border-color: red;}
.compTwo { border-color: green;}
.compThree { border-color: blue;}
.compFour { border-color: black;}
.compFive { border-color: teal;}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id="root"></div>
Edit: New info was added to the question. Given that info, this approach doesn't work.
You can probably solve it using a Higher-order component (HOC), something like this:
const superWrapped = (Outer) => (Middle) => (Inner) => props => {
return (
<Outer>
<Middle>
<Inner content={props.content} />
</Middle>
</Outer>
)
};
Where you would later use it like this:
render() {
const SuperWrapped =
superWrapped(CompOne)(CompThree)(CompTwo); // any order is fine!
return (<SuperWrapped content="Something here.." /> );
}
Some minor adjustments on your components would be necessary for this to work. I've included a working demo below:
const superWrapped = (Outer) => (Middle) => (Inner) => props => {
return (
<Outer>
<Middle>
<Inner content={props.content} />
</Middle>
</Outer>)
};
const CompOne = (props) => {
return (
<div className="compOne">
<p>One:</p>
{props.children || props.content}
</div>
);
};
const CompTwo = (props) => {
return (
<div className="compTwo">
<p>Two:</p>
{props.children || props.content}
</div>
);
};
const CompThree = (props) => {
return (
<div className="compThree">
<p>Three:</p>
{props.children || props.content}
</div>
);
};
class App extends React.Component {
constructor(props) {
super(props);
}
render() {
const components = getComponentOrder();
const SuperWrapped1 =
superWrapped(components[0])(components[1])(components[2]);
const SuperWrapped2 =
superWrapped(components[2])(components[1])(components[0]);
return (
<div>
<SuperWrapped1 content="Hello, world!" />
<SuperWrapped2 content="Goodbye, world!" />
</div>
);
}
}
const getComponentOrder = () => {
return Math.random() < 0.5 ?
[CompOne, CompTwo, CompThree] :
Math.random() < 0.5 ?
[CompThree, CompOne, CompTwo] :
[CompTwo, CompOne, CompThree]
};
ReactDOM.render(<App />, document.getElementById('root'));
.compOne {
border: 5px solid red;
padding: 5px;
margin: 5px;
display: inline-block;
}
.compTwo {
border: 5px solid green;
padding: 5px;
margin: 5px;
display: inline-block;
}
.compThree {
border: 5px solid blue;
padding: 5px;
margin: 5px;
display: inline-block;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id="root"></div>
The easiest way to achieve this is to use a recursive function:
let renderMap = [
{ component: CompOne, props: {} },
{ component: CompTwo, props: {} },
{ component: CompThree, props: { content: "hi there" } }
];
function App() {
let index = 0;
let structure = "Hi There!"
function packing() {
if (index === renderMap?.length) return;
const Comp = renderMap[index].component
structure = <Comp>{structure}</Comp>;
index += 1;
packing();
}
packing();
return <>{structure}</>
}