React js Mapping Menu & Submenus - foreach

Here is my code:
......
render() {
var menuItems = [
{name: 'Item 1', subMenus: [{name: 'Sub Menu 1-2'}, {name: 'Sub Menu 1-2'}, {name: 'Sub Menu 1-3'}]},
{name: 'Item 2'},
{name: 'Item 3'},
{name: 'Item 4', subMenus: [{name: 'Sub Menu 4-2'}, {name: 'Sub Menu 4-2'}, {name: 'Sub Menu 4-3'}]},
{name: 'Item 5'}
]
var menu = function () {
return (
menuItems.forEach(function (menuItem, i) {
if (menuItem.subMenus != undefined) {
<ul key={i}>menuItem.name
menuItem.subMenus.forEach(function (subMenu, i) {
<li key={i}>subMenu.name</li>
}
</ul>
} else {
// do nothing
}
})
)
}
return({menu})
......
Obviously it is not working.
What would be the correct procedure to loop thru a nested menu?
Thanks in advance.

return (
<div>
{/* GJK forgot to encapsulate mapping in curly braces */}
{menuItems.map(function(menuItem, i) {
if (menuItem.subMenus != undefined) {
return (
<ul key={i}>{menuItem.name}
{menuItem.subMenus.map(function(subMenu, i) {
return <li key={i}>{subMenu.name}</li>;
})}
</ul>
)
} else {
return (
<ul key={i}>{menuItem.name}</ul>
)
}
})};
</div>)

You have several problems here:
You're using a forEach() call instead of a map() call. You're creating ul components but you're not doing anything with them, they're just being thrown away.
You're not returning any components from the render method, you're return an object that contains a function that will generate components.
You're not interpolating JS into the JSX correctly. You have to surround JS expressions with brackets.
After you fix #1, you'll be returning a list of components from render when you can only return a single component. I wrapped everything in a div to fix that issue.
This is what you need:
return (
<div>
{menuItems.map(function(menuItem, i) {
if (menuItem.subMenus != undefined) {
return (
<ul key={i}>
{/* Not sure this is what you meant, but I added it anyway */}
{menuItem.name}
{menuItem.subMenus.map(function(subMenu, i) {
return <li key={i}>{subMenu.name}</li>;
})}
</ul>
);
} else {
return undefined;
}
})}
</div>
);

Related

Svelte each worked twice / each binding error ( Cannot set properties of undefined )

I'm trying to bind elements in {#each} block and removing them by click.
<script>
const foodList = [
{ icon: '🍲', elem: null, },
{ icon: '🥫', elem: null, },
{ icon: '🍔', elem: null, },
];
const remove = (index) => {
foodList.splice(index, 1);
foodList = foodList;
};
</script>
{#each foodList as {icon, elem}, index}
<div
bind:this={elems[index]}
on:click={remove}
>
{icon}
</div>
{/each}
In my code i got 2 problems:
{#each} makes twice more iterations than he should do
after removing item in array by clicking on binded element - svelte fires an error "Cannot set properties of undefined"
Why it works like this?
I'm writing this not for asking help, but for people, that will meet same problems
Both of these problems have the same origin, so I gather them here:
some times {#each} block works twice more than expected
some times {#each} block binding throw an error
All code tested in Svelte compiler version 3.44.1
Problem 1 - each worked twice
What do I mean?
each block makes all iterations twice
Just let me show you, look at code below, how many iterations {#each} will do?
<script>
const foodList = [
{ icon: '🍲' },
{ icon: '🥫' },
{ icon: '🍔' }
];
</script>
{#each foodList as {icon}}
<div> {icon} </div>
{/each}
If your answer is - 3, then congrats, you are right.
Ok, now we need to bind elements, that we are rendering in {#each} block.
It's the same code, just added prop 'elem' to objects for each div binding inside {#each}
Look below and try again, how many iterations will make {#each}?
<script>
const foodList = [
{ icon: '🍲', elem: null, },
{ icon: '🥫', elem: null, },
{ icon: '🍔', elem: null, }
];
</script>
{#each foodList as {icon, elem} }
<div
bind:this={elem}
>
{icon}
</div>
{/each}
Right... we got 6 iterations, twice more.
You can see it by adding some console.log() at first {#each} block code, like this:
{#each foodList as {icon, elem}, index}
{console.log('each iteration: ', index, icon) ? '' : ''}
<div
bind:this={elem}
>
{icon}
</div>
{/each}
It happens because we used same array for {#each} iterations and binding.
If we will create new array for binding - the problem will be gone:
<script>
const foodList = [
{ icon: '🍲' },
{ icon: '🥫' },
{ icon: '🍔' }
];
const elems = [];
</script>
{#each foodList as {icon}, index }
{ console.log('each iteration: ', index, icon) ? '' : ''}
<div
bind:this={elems[index]}
>
{icon}
</div>
{/each}
Yeah... now the problem is gone and we got 3 iterations, as we expected.
It's a bug that lives for a long time, as I tried to find out - at least 1 year. This leads to different problems in different circumstances as out code become more complex.
Problem 2 - each binding throws error after iterable array has been sliced
(Something like: 'Cannot set properties of undefined')
What do I mean?
each block binding throws error after we removed one of iterable array items
Same example, just added removing array item on it's element click:
<script>
const foodList = [
{ icon: '🍲', elem: null, },
{ icon: '🥫', elem: null, },
{ icon: '🍔', elem: null, }
];
const remove = (index) => {
foodList.splice(index, 1);
foodList = foodList;
};
</script>
{#each foodList as {icon, elem}, index}
{console.log('each iteration: ', index, icon) ? '' : ''}
<div
bind:this={elem}
on:click={remove}
>
{icon}
</div>
{/each}
We expect that if we make a click on div - array item, to which it was bound will be sliced and we will lose them on screen. Correct... but we got error, because {#each} block make still 3 iterations, not 2 as we were waiting for.
Again for clearness, our code steps: foodList length is 3 -> we make a click on food icon -> foodList length is 2 (we cut item with clicked icon).
After this {#each} should do 2 iterations to render each left icon in foodList, but he did 3!
That's why we have the problem, our code trying to write new property to undefined (item is sliced so when we are trying to read\write it - there is undefined.
// foodList [ { icon: '🍲', elem: null, }, { icon: '🥫', elem: null, }]
foodList[2].elem = <div>; // "Cannot set properties of undefined"
It's a bug and it happens if we used same array for {#each} iterations and binding.
The most clean fix on my question is to separate iterable and binding data into different arrays:
<script>
const foodList = [
{ icon: '🍲' },
{ icon: '🥫' },
{ icon: '🍔' }
];
let elems = [];
const remove = (index) => {
foodList.splice(index, 1);
foodList = foodList;
};
</script>
{#each foodList as {icon, cmp}, index}
{console.log('each iteration: ', index, icon) ? '' : ''}
<div
bind:this={elems[index]}
on:click={remove}
>
{icon}
</div>
{/each}
But... Let's look inside out new elems array by adding $: console.log(elems);
(it's reactive expression, that will print elems array each time as it changes)
<script>
const foodList = [
{ icon: '🍲' },
{ icon: '🥫' },
{ icon: '🍔' }
];
let elems = [];
const remove = (index) => {
foodList.splice(index, 1);
foodList = foodList;
};
$: console.log(elems);
</script>
{#each foodList as {icon, cmp}, index}
{console.log('each iteration: ', index, icon) ? '' : ''}
<div
bind:this={elems[index]}
on:click={remove}
>
{icon}
</div>
{/each}
Looks like a have 2 news for you
we got no error
we have new null item in elems array
It means that problem is still here( {#each} block makes still 1 extra iteration for sliced item).
For now we can filter elems array after foodList slicing, just do it after page update, such as tick().
Full code:
<script>
import { tick } from 'svelte';
const foodList = [
{ icon: '🍲', elem: null, },
{ icon: '🥫', elem: null, },
{ icon: '🍔', elem: null, }
];
let elems = [];
const remove = async (index) => {
foodList.splice(index, 1);
foodList = foodList;
await tick();
elems = elems.filter((elem) => (elem !== null));
};
$: console.log(elems);
</script>
{#each foodList as {icon, elem}, index}
{console.log('each iteration: ', index, icon) ? '' : ''}
<div
bind:this={elems[index]}
on:click={remove}
>
{icon}
</div>
{/each}
Keep in mind: {#each} block still works 1 extra time and we got null as bound elem, we just filtered it after the page updates.
Last stand
Don't know what to say for real... I wasted too much time on this **** trying to figure out why my code isn't work as it should be.
I like svelte, but I don't like bugs
I really hope this little guide will helps some of you to save a lot of time.
Will be glad to your corrections, see you and don't let 🐞 win.
P.S.
Yep, it takes time, but... Never know when you will needs help, share your knowledge

jquery_ui menu ui parameter for dynamic sub-menu

I am trying to create a dynamic sub-menu. For example, the top-level items are load and edit. Whenever the menu is focused, a get JSON is called and the result used to populate the possible items to load under the load menu item. However, the ui parameter for the select event handler no longer seems to correspond to the item clicked and calling ui.item.text() will instead return the entire text to the sub-menu.
HTML:
...
<ul id="menu">
<li><div>load</div><ul id="load"></ul></li>
<li><div>edit</div></li>
</ul>
...
javascript:
var load_populate = function ( json ) {
var ul = $( "#load" );
ul.empty();
for ( let item of json ) {
ul.append ( $( "<li>" ).append ( $( "<div>" ).text ( item.name ) ) );
}
};
var menu_focus = function ( event, ui ) {
$.getJSON ( load_options_url )
.done ( load_populate );
};
var menu_select = function ( event, ui ) {
console.log ( ui.item.text() );
};
$( "#menu" ).menu ({
"focus" : menu_focus,
"select" : menu_select
});
Clicking an item in the load sub-menu logs
loaditem1item2item3, etc.
Refreshing the menu did not work.
How do I go about this?
Here is an example based on your code.
Example Fiddle
This uses POST just to simulate the AJAX activity
https://jsfiddle.net/Twisty/qpky9ht1/53/
HTML
<ul id="menu">
<li>
<div>load</div>
<ul id="load">
</ul>
</li>
<li>
<div>edit</div>
</li>
</ul>
JavaScript
Adjusted to be more similar to your code.
$(function() {
function makeListItem(o, n) {
$(o).append("<li><div>" + n + "</div></li>");
}
$("#menu").menu({
focus: function(e, ui) {
console.log("FOCUS", e, ui.item);
if (ui.item.parent().is("#menu")) {
$.getJSON (load_options_url,
function(json) {
console.log("LOAD");
$("#load").empty();
$.each(json, function(i, m) {
console.log("ITEM", i, m);
makeListItem("#load", m.name);
});
$("#menu").menu("refresh");
});
}
},
select: function(e, ui) {
console.log("SELECT", ui.item);
}
});
});
This will gather the list data in focus and append them. Once this is done, refresh method is executed on the Menu. This will assimilate all the new list items into the menu. Since focus can be executed on all menu items, you want to ensure you are not rebuilding the menu while focusing on sub-menu items. Hence the if statement.

how to avoid key/id problems in reactjs and make props pass from parent to child?

I keep hitting a wall when trying to get the parent data passed down to the child component.
My view:
<%= react_component 'Items', { data: #items } %>
My Items component makes an ajax call, sets state, and renders Item. Leaving key={this.props.id} out of the Item instance passed into the mapping function makes it so that the component html renders to the page. But add the key in, and I get a console error: Uncaught TypeError: Cannot read property 'id' of undefined
Here's 'Items':
var Items = React.createClass({
loadItemsFromServer: function() {
$.ajax({
url: this.props.url,
dataType: 'json',
cache: false,
success: function(data) {
this.setState({data: data});
}.bind(this),
error: function(xhr, status, err) {
console.error(this.props.url, status, err.toString());
}.bind(this)
});
},
componentDidMount: function() {
this.loadItemsFromServer();
},
render: function() {
var itemNodes = this.props.data.map(function() {
return (
<Item key={this.props.id} />
);
});
return (
<div className="ui four column doubling stackable grid">
{itemNodes}
</div>
);
}
});
My item.js.jsx component just formats each Item:
var Item = React.createClass({
render: function() {
return (
<div className="item-card">
<div className="image">
</div>
<div className="description">
<div className="artist">{this.props.artist}</div>
</div>
</div>
);
}
});
The React dev tools extension shows the props and state data inside Items. The children, however, are empty.
I'm aware of this, but I'm setting key with this.props.id. I'm not sure what I'm missing?
I found a couple of problems with the code you posted, in the Items component
You're rendering this.props.data while in fact this.state.data is the one being updated with the ajax request. You need to render this.state.data but get the initial value from props
The map iterator function takes an argument representing the current array element, use it to access the properties instead of using this which is undefined
The updated code should look like this
var Item = React.createClass({
render: function() {
return (
<div className="item-card">
<div className="image">
</div>
<div className="description">
<div className="artist">{this.props.artist}</div>
</div>
</div>
);
}
});
var Items = React.createClass({
getInitialState: function() {
return {
// for initial state use the array passed as props,
// or empty array if not passed
data: this.props.data || []
};
},
loadItemsFromServer: function() {
var data = [{
id: 1,
artist: 'abc'
}, {
id: 2,
artist: 'def'
}]
this.setState({
data: data
});
},
componentDidMount: function() {
this.loadItemsFromServer();
},
render: function() {
// use this.state.data not this.props.data,
// since you are updating the state with the result of the ajax request,
// you're not updating the props
var itemNodes = this.state.data.map(function(item) {
// the map iterator function takes an item as a parameter,
// which is the current element of the array (this.state.data),
// use (item) to access properties, not (this)
return (
// use key as item id, and pass all the item properties
// to the Item component with ES6 object spread syntax
<Item key={item.id} {...item} />
);
});
return (
<div className="ui four column doubling stackable grid">
{itemNodes}
</div>
);
}
});
And here is a working example http://codepen.io/Gaafar/pen/EyyGPR?editors=0010
There are a couple of problems with your implementation.
First of all, you need to decide: Do you want to render the #items passed to the Items component from your view? Or do you want to load them asynchronous?
Because right now I get the impression you are trying to do both...
Render items passed from view
If you want to render the items from your view passed to the component, make sure it's proper json. You might need to call 'as_json' on it.
<%= react_component 'Items', { data: #items.as_json } %>
Then, in your Component, map the items to render the <Item /> components. Here is the second problem, regarding your key. You need to define the item variable to the callback function of your map function, and read the id from it:
var itemNodes = this.props.data.map(function(item) {
return (
<Item key={item.id} artist={item.artist} />
);
});
Note, I also added the author as prop, since you are using it in your <Item /> Component.
You can remove your componentDidMount and loadItemsFromServer functions, since you are not using them.
Load items asynchronous
If you want to load the items asynchronously, like you are trying to do in your loadItemsFromServer function, first of all, pass the url from your view and remove the {data: #items} part, since you will load the items from your component, something like:
<%= react_component 'Items', { url: items_path(format: :json) } %>
If you want to render the asynchronous fetched items, use:
var itemNodes = this.state.data.map(function(item) {
return (
<Item key={item.id} artist={item.artist} />
);loadItemsFromServer
});
Note I changed this.props.map to this.state.map
You can now use your componentDidMount and loadItemsFromServer functions to load the data and save them to state.

React / Rails : Append dynamically element to DOM

Currently following facebook tutorial on React (react_tuto).
I don't understand how 2 components can communicate so that on "submit a comment button" it appends dynamically the "comment list".
currently, comment are created on server but appears on page only when page refreshed
how can the comment appear on submit button?
This i my AddComment component
var AddComment = React.createClass({
getInitialState: function(){
return {
content: this.props.content,
adrien: "before"
}
},
handleKeyUp: function(e) {
this.setState({
content: this.refs.addComment.getDOMNode().value,
})
},
handleValidation: function() {
var that = this
$.ajax({
type: "POST",
data: {comment: { content: that.state.content } },
url: Routes.create_comment_path({format: 'json'}),
success: function(data) {
that.setState({
content: "",
adrien: "after"
})
}
})
},
render: function(){
return (
<div>
<textarea onKeyUp={this.handleKeyUp} value={this.state.value} ref="addComment"></textarea>
<button onClick={this.handleValidation}>submit</button>
</div>
)
}
})
This is my CommentList component:
var CommentList = React.createClass({
render: function() {
return (
<div>
{this.props.comments.map(function(comment){
return <CommentListElement key={comment.id} comment={comment} />;
})}
</div>
);
}
});
You need a common parent component for communication between different components.
I have updated you example a bit to include common parent component CommentSystem
Note: I have removed ajax call to just show the communication between component.
Check below link.
https://jsfiddle.net/j4yk3pzc/15/
Extra Info:
In react we store states on parent component and pass them down to children. Along with state we also pass actions to manipulate data down to the children. When child component want's to update data passed to it from parent, then it fires the action passed from the parent. This is called Data down action up approach. Data is passed from parent to child to grandchild. While actions are propagated from grandchild to child to parent.
If you don't want to create the parent component then you can use some Publish / Subscribe or EventEmitter based system to communicate between children having no common parent.
Reference:
http://ctheu.com/2015/02/12/how-to-communicate-between-react-components/
Code:
var CommentSystem = React.createClass({
getInitialState: function() {
return {
comments: []
}
},
addComments: function(comment) {
var comments = this.state.comments;
comments.push(comment);
this.setState({comments: comments})
},
render: function() {
return (
<div>
<AddComment addComments={this.addComments}/>
<CommentList comments={this.state.comments}/>
</div>
)
}
})
var AddComment = React.createClass({
getInitialState: function(){
return {
content: this.props.content,
adrien: "before"
}
},
handleKeyUp: function(e) {
this.setState({
content: this.refs.addComment.getDOMNode().value,
})
},
handleValidation: function() {
var that = this;
this.props.addComments(this.state.content);
},
render: function(){
return (
<div>
<textarea onKeyUp={this.handleKeyUp} value={this.state.value} ref="addComment"></textarea>
<button onClick={this.handleValidation}>submit</button>
</div>
)
}
})
var CommentList = React.createClass({
render: function() {
return (
<div>
{this.props.comments.map(function(comment){
return <CommentListElement key={comment.id} comment={comment} />;
})}
</div>
);
}
});
var CommentListElement = React.createClass({
render: function() {
return (
<div>{this.props.comment}</div>
)
}
})
React.render(<CommentSystem/>, document.getElementById('container'));
Hope this helps.

JQuery UI Autocomplete and hidden field

I have implemented the JQuery UI Autocomplete widgets in my form so when the user selects an item from the autocomplete, a hidden field is populated with the id and the textbox with the user friendly text.
I have also implemented a remote validation so if the user after selecting an item from the list (hidden field already set) decides to delete or change the text it will fail and force the user to select an item again.
I do want to allow the user to delete the field so if all content of the textbox is deleted I want to reset the hidden field, but I don't know how to do this...
Thanks in advance.
Typically, you can catch certain events on the input field that is the Autocomplete widget. The main ones would be blur where the input field loses focus and or keyup where someone has hit enter inside the input field (maybe trying to submit the form).
The following example shows how a "hidden" field could be updated in these two scenarios:
$(function() {
var availableTags = [
"ActionScript",
"AppleScript",
"Asp",
"BASIC",
"C",
"C++",
"Clojure",
"COBOL",
"ColdFusion",
"Erlang",
"Fortran",
"Groovy",
"Haskell",
"Java",
"JavaScript",
"Lisp",
"Perl",
"PHP",
"Python",
"Ruby",
"Scala",
"Scheme"
];
function split( val ) {
return val.split( /,\s*/ );
}
function extractLast( term ) {
return split( term ).pop();
}
$( "#tags" )
// don't navigate away from the field on tab when selecting an item
.bind( "keydown", function( event ) {
if ( event.keyCode === $.ui.keyCode.TAB &&
$( this ).autocomplete( "instance" ).menu.active ) {
event.preventDefault();
}
})
.autocomplete({
minLength: 0,
source: function( request, response ) {
// delegate back to autocomplete, but extract the last term
response( $.ui.autocomplete.filter(
availableTags, extractLast( request.term ) ) );
},
focus: function() {
// prevent value inserted on focus
return false;
},
select: function( event, ui ) {
var terms = split( this.value );
// remove the current input
terms.pop();
// add the selected item
terms.push( ui.item.value );
// add placeholder to get the comma-and-space at the end
terms.push( "" );
this.value = terms.join( ", " );
$("#hiddenField").val(terms.join( ", "));
return false;
}
});
});
$("#tags").blur(function() {
$("#hiddenField").val(this.value);
});
$("#tags").keyup(function (e) {
if (e.keyCode == 13) {
$("#hiddenField").val(this.value);
}
});
.hiddenFields {
margin-bottom: 30px;
}
.hiddenF {
color: gray;
}
.hiddenI {
color: gray;
border: 1px dotted gray;
opacity: 50%;
}
<link rel="stylesheet" href="//code.jquery.com/ui/1.11.1/themes/smoothness/jquery-ui.css">
<script src="//code.jquery.com/jquery-1.10.2.js"></script>
<script src="//code.jquery.com/ui/1.11.1/jquery-ui.js"></script>
<div class="hiddenFields">
<label for="hiddenField" class="hiddenF">Hidden Field: </label>
<input id="hiddenField" class="hiddenI" size="50" readonly/>
</div>
<div class="ui-widget">
<label for="tags">Tag programming languages: </label>
<input id="tags" size="50">
</div>

Resources