How can I traverse through my data structure and create React components?
{
component: "View",
attributes: {
id: 'main'
},
child: [
{
component: "Text",
content: "Hello World!"
},
{
component: "Link",
attributes: {
href: "#"
},
child: [
{
component: "Text",
content: "Click me!"
}
]
}
]
}
Would output:
<View>
<Text>Hello World!</Text>
<Link>Click me!</Link>
</View>
How can I dynamically achieve this where it works regardless of the number of nested components?
I am able to make the top level component, but traversing through the child elements is where I hit a brick wall.
You can create a function that calls itself.
Sample
parseComponents = (data, key) => {
if (key === undefined || key === null) key = 0;
let Component = null;
switch(data.component) {
case 'View':
Component = View;
break;
case 'Text':
Component = Text;
break;
case 'Link':
Component = Link;
break;
default:
break;
}
if (Component === null) return Component;
return (
<Component key={`${data.component}-${index}`} {...data.attributes}>
{data.child.map((c, index) => this.parseComponents(c, index))}
</Component>
)
}
You can do like sample below:
Let's say your JSON is stored in const json.
getComponent = (json) => {
if (json.component) {
let children;
if (json.children) {
children = json.children.map(child => this.getComponentEquivalent(child));
}
let Container= this.getComponentEquivalent(json);
return (<Container>{children}</Container>); // as this will return a component you can directly put this in render.
}
};
then you may have a function that you could get equivalent component.
getComponentEquivalent = (object) => {
switch(object.component) {
case "Text":
return <Text>{object.content}</Text>
case "Link"":
return <Link>{object.content}</Link>
//......./
default:
//..../
}
};
Related
I have a problem with Typescript with React and useReducer hook. The state does not wanted to be updated and it is 100% typescript problem (I mean I need a typescript solution because in javascript it works perfect). So I want to make reducer as short as possible so I use "name" in HTML and get this name as a name of object in initialState. When I return ( title:{}, description: {}) It works but when I use [action.field] it does not work. action.field is the name of HTML element.
const initialStateReducer: inputsFormState = {
title: {
val: "",
isValid: false,
},
description: {
val: "",
isValid: false,
},
};
const RecipeForm = () => {
const inputReducer = (
state: inputsFormState,
action: inputsFormAction
): inputsFormState => {
console.log(action.type, action.content, action.field);
let isValid: boolean;
const { content } = action;
isValid = content.length > 0;
return {
[action.field]: {
val: content,
isValid: isValid,
},
...state,
};
};
const [formIsValid, setFormIsValid] = useState<boolean>(false);
const [inputsValues, dispatchReducer] = useReducer(
inputReducer,
initialStateReducer
);
const changeTextHandler = (
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
) => {
dispatchReducer({
type: ActionKind.changeVal,
field: e.target.name,
content: e.target.value,
});
};
return (
<React.Fragment>
<Input
name="title"
onChange={(e) => changeTextHandler(e)}
placeholder="Name for your recipe"
/>
<Textarea
name="description"
onChange={(e) => changeTextHandler(e)}
placeholder="Description"
cols={20}
rows={20}
resize="none"
/>
</React.Fragment>
);
};
Typescript is only a superset of JS that adds on Typing while writing code, it has no effect on the actually running of the JS (as it gets compiled into JS before running).
From looking at the above code I think the issue is your return in the reducer:
return {
[action.field]: {
val: content,
isValid: isValid,
},
...state,
};
Should be:
return {
...state,
[action.field]: {
val: content,
isValid: isValid,
}
};
As (and this may not be a great explanation) but right most arguments overwrite the preceding values, so you're effectively overwriting the new with the original.
I created a todo list by using react. I get some problem that I want to create checkbox but my checkbox it does not work and I cannot solve :( I don't know what's wrong with that.
I set the data for each task and then I need to change the completed of some task, but it cannot click and change the completed task
This is my code
class App extends React.Component {
constructor() {
super()
this.state = {
todos: todoData,
}
this.handleChange = this.handleChange.bind(this)
}
handleChange(id) {
this.setState(prevState => {
const updatedTodos = prevState.todos.map(todo => {
if(todo.id === id) {
todo.completed = !todo.completed
// console.log(todo.completed)
}
return todo
})
return {
todos: updatedTodos
}
})
}
render() {
const todoItem = this.state.todos.map(item => <TodoItem key={item.id} item={item}
handleChange={this.handleChange}/>)
return (
<div>
<h1 className="header">My Todo Lists</h1>
{todoItem}
</div>
)
}
}
function TodoItem(props) {
let textItem = props.item.completed === true ?
<del>{props.item.text}</del> : props.item.text
return (
<div className="list">
<input
type="checkbox"
checked={props.item.completed}
onChange={() => props.handleChange(props.item.id)}
/>
<p className="item">{textItem}</p>
</div>
)
}
And this is my data
const todoData = [
{
id: 1,
text: "Practice coding",
completed: false
},
{
id: 2,
text: "Grocery shopping",
completed: true
},
{
id: 3,
text: "Wash the dishes",
completed: true
},
{
id: 4,
text: "Take out the trash",
completed: false
},
{
id: 5,
text: "Teach my brother homework",
completed: false
}
]
Thank you for helping :)
Looks like on your handleChange you are mutating the existing state on your map transformation. you must return a new state instead.
Replace your handleChange with the following code:
handleChange(id) {
this.setState((prevState) => {
const updatedTodos = prevState.todos.map((todo) => {
return {
...todo,
completed: todo.id === id ? !todo.completed : todo.completed
};
});
return {
todos: updatedTodos
};
});
}
I'm trying to access object inside an array of a JSONObject and print its values.
I'm able to print the array as JSONObject using console.log. But i fail to access the values inside the array which are again JSONObject format. Following is my my JSONObject
{
"id": 4,
"meta": {
"type": "pagetype",
"title": "Home"
}
},
"title": "Expose data to frontend",
"subtitle": "We will be exposing the content to the frontend",
"content": [
{
"type": "full_richtext",
"value": "<p><b>Bold body</b></p>"
},
{
"type": "button",
"value": {
"button_text": "Google",
"button_url": "https://google.com"
}
}
]
}
I need to access the values inside the array "content" and print values for
"value" -- Bold body --
"button_text"
"button_url"
I have tried it as follows
class App extends React.Component {
constructor() {
super();
this.state = {
'items': []
}
}
componentDidMount() {
fetch('http://localhost:8000/api/v2/pages/4/')
.then(results => results.json())
.then(results => this.setState({ 'items': results }));
}
render() {
var contents_from_wagtail = this.state.items;
var streamfield_content_array = contents_from_wagtail.content;
console.log(streamfield_content_array); //prints array of objects
return (
<React.Fragment>
<p>{this.state.items.subtitle}</p>
<p>{this.state.items.title}</p>
/* print the values for
"value" -- Bold body --
"button_text"
"button_url"
*/
</React.Fragment>
);
}
}
export default App;
When showing an array of items the .map method can be used to create multiple elements:
class App extends React.Component {
constructor() {
super();
this.state = {
'items': {}
}
}
componentDidMount() {
fetch('http://localhost:8000/api/v2/pages/4/')
.then(results => results.json())
.then(results => this.setState({ 'items': results }));
}
render() {
var contents_from_wagtail = this.state.items;
var streamfield_content_array = contents_from_wagtail.content || [];
console.log(streamfield_content_array); //prints array of objects
return (
<React.Fragment>
<p>{this.state.items.subtitle}</p>
<p>{this.state.items.title}</p>
{streamfield_content_array.map((item, index) => {
return <div key={index}>type: {item.type} <p>{item.value}</p></div>
})}
</React.Fragment>
);
}
}
export default App;
More .map examples: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map
To access it within the render you have to access it conditional because it is not set for the first render until the fetch call is Executed
That is why you have to provide some fallback until the array is loaded.just check if the item is undefined and return null for example.
Only if the array is filled render the desires output and it should be fine.
Hope this helps. Happy coding.
You could use a combination of .map() and .filter() to iterate over the items within the content array. It looks like you only want to display items that have a type of button. So try something like this:
class App extends React.Component {
constructor() {
super();
this.state = {
'items': []
}
}
componentDidMount() {
fetch('http://localhost:8000/api/v2/pages/4/')
.then(results => results.json())
.then(results => this.setState({ 'items': results }));
}
render() {
var contents_from_wagtail = this.state.items;
var streamfield_content_array = contents_from_wagtail.content;
var buttonContent = this.state.items ? this.state.items.content.filter((item) => item.type == "button") : null
return (
<React.Fragment>
<p>{this.state.items.subtitle}</p>
<p>{this.state.items.title}</p>
{ buttonContent && buttonContent.map(item => (
<div>
<p>{item.button_text}</p>
<p>{item.button_url}</p>
</div>
))}
</React.Fragment>
);
}
}
export default App;
I have json data passed as props from the Redux store, the json has this format:
{
"label1.label2.label3": {
"option1":[
],
"option2": "value",
"option3": [
],
...
}
...
}
I need to split the label part and create a nested menu from the data as follows:
label1
label2
label3
option1
value of option1
option2
value of option2
option3
...
I couldn't figure out how to map into the data, my component code looks like this:
class MyDropdown extends React.Component {
componentDidMount(){
//A component dispatches ACTIONS
this.props.dispatch(fetchDataWithRedux());
}
render(){
const {error, loading, elements} = this.props;
if (loading){
return <div>Loading...</div>;
}
if (error){
return <div> Error! {error.message}</div>;
}
console.log(this.props.elements); //outputs "undefined"
return(
<div>
{this.props.elements && this.props.elements.map(this.createMenu)}
</div>
);
}
createMenu() {
return this.props.elements.map((item) => {
return(
);
});
}
}
const mapStateToProps = state => ({
elements: state.items,
loading: state.loading,
error: state.error
});
export default connect(mapStateToProps)(MyDropdown);
I'm getting real close with this. It's pretty tricky to get the nesting right though.
const data = {
"label1.label2.label3": {
"option1":"value1",
"option2": "value2",
"option3": "value3"
},
"label1.label2.label3.label4": {
"option1":"value1",
"option2": "value2"
},
}
class App extends React.Component {
createMenu = () => {
const options = Object.keys(data).reduce( (options, key, i) => {
const d = data[key];
let levels = key.split('.').map( (label, i) => {
return {
label: label,
options: []
};
});
levels[levels.length -1].options = Object.keys(d).map( option => {
return {
label: option,
value: d[option]
}
});
options.push(levels);
return options;
}, []);
const mapOptions = o => {
let menu = [];
o.forEach( (option, i) => {
if(!option.options.length) {
// Nest next option
menu.push(
<li>
{option.label}
</li>
);
}else {
// display options
menu.push(
<li>
{option.label}
{option.options.map( nested => {
return <li>{nested.label}: {nested.value}</li>
})}
</li>
);
}
});
return menu;
};
return options.map( option => {
return mapOptions(option);
});
};
render() {
return (
<div>
{this.createMenu()}
</div>
)
}
}
ReactDOM.render(<App/>, document.getElementById('root'));
li li {
margin-left: 15px;
}
<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>
Feels like I'm missing something obvious here - but I can't figure out how to access my JSON data. I have a Container component:
class About extends Component {
componentDidMount(){
const APP_URL = 'http://localhost/wordpress/'
const PAGES_URL = `${APP_URL}/wp-json/wp/v2/pages`
this.props.fetchAllPages(PAGES_URL, 'about')
}
render(){
return (
<div>
<Header/>
<div className="bg">
<div className="home-wrapper">
<h1>AAAAABBBBBOOOOUUUUUT</h1>
<Counter/>
<AboutInfo />
</div>
</div>
<Footer/>
</div>
)
}
}
const mapDispatchToProps = (dispatch) => {
return bindActionCreators({ fetchAllPages }, dispatch)
}
export default connect(null, mapDispatchToProps)(About);
And a Smart component:
class AboutInfo extends Component {
render(){
console.log(this.props.page);
console.log(this.props.page.id);
return (
<div>
<h1>This is ID: {this.props.page.id}</h1>
</div>
)
}
}
const mapStateToProps = ({ page }) => {
return { page }
}
export default connect(mapStateToProps)(AboutInfo);
My action:
export const fetchAllPages = (URL, SLUG) => {
var URLEN;
if(!SLUG){
URLEN = URL
} else {
URLEN = URL + "?slug=" + SLUG
}
return (dispatch) => {
dispatch(fetchRequest());
return fetchPosts(URLEN).then(([response, json]) => {
if(response.status === 200){
if(!SLUG) {
dispatch(fetchPagesSuccess(json))
} else {
dispatch(fetchPageBySlugSuccess(json))
}
} else {
dispatch(fetchError())
}
})
}
}
const fetchPageBySlugSuccess = (payload) => {
return {
type: types.FETCH_PAGE_BY_SLUG,
payload
}
}
My reducer:
const page = (state = {}, action) => {
switch (action.type) {
case FETCH_PAGE_BY_SLUG:
console.log(action.paylod)
return action.payload
default:
return state
}
}
This gives me:
When I console.log(this.props.page) in my AboutInfo component, it prints the object, but when I print console.log(this.props.page.id) it gives me undefined. Why can't I print the JSON content? Thanks!
page is an array and hence this.props.page.id is undefined. You might want to access the first element in array in which case you would do
this.props.page[0].id
but you might also need to add a test, since before the response is available you will be trying to access page[0].id and it might break.
You could instead write
this.props.page && this.props.page[0] && this.props.page[0].id
Getting data from the store is async So you must adding loading varibale on your reducer
class AboutInfo extends Component {
render(){
if(this.props.loading) return (<div>loading</div>);
return (
<div>
<h1>This is ID: {this.props.page.id}</h1>
</div>
);
}
}
const mapStateToProps = ({ page, loading }) => {
return { page, loading }
}
on your action try returing
json.page[0]
That is because page is an array and the id is a property of its 1st element.
So use this.props.page[0].id
If the logged object in your screenshot is the this.props.page then you will need and additional .page as that is also a part of the object this.props.page.page[0].id