I am trying to create a navigation menu which supports dropdown items. I have created
var menuItems={
items:[
{
title:"Home",
id:1
},
{
title:"Download App",
children:["iOS","Android","Windows"],
id:2
},
{
title:"About",
id:3
}
]
};
This is my Menu Prototype Class:
var MenuProto = React.createClass({
render: function(){
return <li><a>{this.props.title}</a>
<ul className="dropdown-menu">
<li><a>{this.props.children}</a></li>
</ul>
</li>
}
});
And this is where it is called:
var Menu = React.createClass({
render : function(){
var fullmenu = this.props.items.map(function(item){
return <MenuProto {...item}/>
});
return <ul className="nav navbar-nav navbar-right">
{fullmenu}
</ul>
}
});
Apparently this.props.children gets all the array and uses renders it on the menu while I need it to get them one by one. I have tried creating a var similar to fullmenu which would iterate through childrens of this.props.children one by one but I can't figure out how.
I would also need a hint on how to make a conditional to see if an object has or doesnt have the children key.
Any help would be greatly appreciated.
You can iterate on this.props.children using the React.Children utilities:
var MenuProto = React.createClass({
render: function(){
return <li><a>{this.props.title}</a>
<ul className="dropdown-menu">
{React.Children.map(this.props.children, function(child) {
return <li key={child.id}><a>{child.title}</a></li>
})}
</ul>
</li>
}
});
Since you want to support nested items, you'll need some additional custom logic.
You can check to see if an object has a children key by simply testing for the existence of whatever.children; if it doesn't have that key, it will be a falsy value (specifically undefined).
Related
I am trying to handle a list of input dynamically. However when I put some content inside my inputs and delete one from my list I have a undesired behaviour. The correct item is removed but the item in the last position takes the state of the previous item.
var ListItem = React.createClass({
getInitialState: function() {
return {
content: ''
}
},
handleChange: function(e) {
this.setState({
content: e.target.value
})
},
render: function() {
return <div>
<input type="text" onChange={this.handleChange} />
</div>
}
});
var App = React.createClass({
getInitialState : function(){
return {
items : []
}
},
deleteElement: function(index, e) {
this.setState({
items: this.state.items.filter(function (e, i) {
return i !== index;
})
});
},
addElement: function() {
this.setState({
items: this.state.items.concat(<ListItem />)
});
},
render: function() {
var list = this.state.items.map(function(item, i) {
return <li key={ i }>
<p onClick={ this.deleteElement.bind(this, i) }>(-)</p>
<span>{ item }</span>
</li>
}, this);
return <div>
<ul>{ list }</ul>
<p onClick={ this.addElement }>(+)</p>
</div>
}
});
ReactDOM.render(
<App />,
document.getElementById('container')
);
<script src="https://facebook.github.io/react/js/jsfiddle-integration-babel.js"></script>
<div id="container">
<!-- This element's contents will be replaced with your component. -->
</div>
I think that the issue could be about my list keys but I do not understand why. How could I remove my list elements without altering any states ?
Thanks
When iterating in render, React reuses component instances (for obvious performance reasons). To discriminate between instances, the prop key is used.
In your case when you remove an element, indexes move and therefore you mess with the key -> instance association. So list items after the deleted element are not associated with the right instance anymore, that is why some of your list elements seem to have the state of the previous element.
To avoid this problem you should never use index as key prop. Use a unique identifier for the data instance instead.
In your case, you could generate a unique identifier when adding a new element, and use it as key.
For more information about how lists work in React, read: https://facebook.github.io/react/docs/lists-and-keys.html
In AngularJS 1.5, I want to pass a binding from a component into the (multi-slot) transcluded scope - for a reference in the template (in either one specific or all of them - no either way is fine).
This is for creating a generic custom-select list
// Component
.component('mySelect', {
bind: {
collection: '<'
},
transclude:{
header: 'mySelectHeader',
item: 'mySelectItem'
},
templateUrl: 'my-select-template',
controller: function(){
.....
}
});
...
// Component template
<script id="my-select-template" type="text/ng-template">
<ol>
<li ng-transclude="header"> </li>
<li ng-transclude="item"
ng-click="$ctrl.select($item)"
ng-repeat"$item in $ctrl.collection">
</li>
</ol>
</script>
...
// Example usage
<my-select collection="[{id: 1, name: "John"}, {id: 2, name: "Erik"}, ... ]>
<my-select-head></my-select-head>
<!-- Reference to $item from ng-repeate="" in component -->
<my-select-item>{{$item.id}}: {{$item.name}}</my-select-item>
</my-select>
Is this possible from a .component()? with custom-directives for the transclusion ?
In your parent component my-select keep a variable like "selectedItem"
In your child component my-select-item, require your parent component like below
require: {
mySelect: '^mySelect'
}
And in your my-select-item component's controller, to access your parent component
$onInit = () => {
this.mySelectedItem= this.mySelect.selectedItem; // to use it inside my-select-item component.
};
select($item) {
this.mySelect.selectedItem = $item; // to change the selectedItem value stored in parent component
}
So that the selected item is now accessible from
<my-select-item>{{selectedItem.id}}: {{selectedItem.name}}</my-select-item>
I ran into this problem as well, and building upon salih's answer, I came up with a solution (disclaimer--see bottom: I don't think this is necessarily the best approach to your problem). it involves creating a stubbed out component for use in the mySelect component, as follows:
.component('item', {
require: { mySelect: '^mySelect' },
bind: { item: '<' }
})
then, tweaking your template:
<script id="my-select-template" type="text/ng-template">
<ol>
<li ng-transclude="header"> </li>
<li ng-click="$ctrl.select($item)"
ng-repeat"$item in $ctrl.collection">
<item item="$item" ng-transclude="item"></item>
</li>
</ol>
</script>
this will mean there's always an item component with the value bound to it. now, you can use it as a require in a custom component:
.component('myItemComponent', {
require: {
itemCtrl: '^item',
}
template: '<span>{{$ctrl.item.id}}: {{$ctrl.item.name}}</span>',
controller: function() {
var ctrl = this;
ctrl.$onInit = function() {
ctrl.item = ctrl.itemCtrl.item;
}
}
});
and to use it:
<my-select collection="[{id: 1, name: "John"}, {id: 2, name: "Erik"}, ... ]>
<my-select-head></my-select-head>
<my-select-item>
<my-item-component></my-item-component>
</my-select-item>
</my-select>
after I implemented this, I actually decided to change my strategy; this might work for you as well. instead of using a transclude, I passed in a formatting function, i.e.:
.component('mySelect', {
bind: {
collection: '<',
customFormat: '&?'
},
transclude:{
header: 'mySelectHeader'
},
templateUrl: 'my-select-template',
controller: function(){
var ctrl = this;
ctrl.format = function(item) {
if(ctrl.customFormat) {
return customFormat({item: item});
} else {
//default
return item;
}
}
.....
}
});
then in the template, just use:
<script id="my-select-template" type="text/ng-template">
<ol>
<li ng-transclude="header"> </li>
<li ng-click="$ctrl.select($item)"
ng-repeat"$item in $ctrl.collection"
ng-bind="::$ctrl.format($item)">
</li>
</ol>
</script>
let me know if you have any feedback or questions!
Im new to the whole world of React JS.. I have two components which share functionality through a mixin. Its to handle a menu which opens and closes.. It works fine when i click the button and the state turn to true and false. Inside the second component I want to use the close function to close the menu when clicking on the shadow element. The state updates after calling the close function. But when click the button to open menu the state is still false..
Any ideas?
---Mixin---
var menuMixin = {
navIcon: $('#nav-icon'),
menu: $('#menu'),
htmlElement: $('html'),
header: $('header'),
getInitialState: function() {
return {
state: false
};
},
openMenu: function(){
var THIS = this;
var $navIcon = $('#nav-icon'),
$menu = $('#menu'),
$html = $('html'),
$header = $('header');
$menu.show();
$navIcon.addClass('open');
setTimeout(function(){
THIS.switchLogo(true);
$menu.addClass('active');
$html.addClass('fixed');
$header.removeClass('active');
THIS.setState({state: true});
}, 255);
},
closeMenu: function() {
var THIS = this;
var $navIcon = $('#nav-icon'),
$menu = $('#menu'),
$html = $('html'),
$header = $('header');
$menu.removeClass('active');
$navIcon.removeClass('open');
this.switchLogo(false);
setTimeout(function(){
$menu.hide();
$html.removeClass('fixed');
THIS.setState({state: false});
if ( $(document).scrollTop() > 200 ) {
$header.addClass('active');
}
}, 255);
}
};
--Nav Button--
var NavButton = React.createClass({
mixins:[logoSwitchlMixin, menuMixin],
handleClick: function() {
console.log('handleClick -->' + this.state.state);
if ( !this.state.state ) {
this.openMenu();
}
else {
this.closeMenu();
}
},
render: function() {
return (
<button id="nav-button">
<div id="nav-icon" onClick={this.handleClick} ref="icon">
<span></span>
<span></span>
<span></span>
</div>
</button>
)
}
});
-- Sliding Menu --
var MainNav = React.createClass({
mixins:[logoSwitchlMixin, menuMixin],
scroll: function(target) {
$('html, body').animate({
scrollTop: $( '#'+ target ).offset().top
}, 600);
},
scrollSection: function(e){
e.preventDefault();
this.scroll( $(e.target).data('id') );
},
render: function() {
return (
<div id="menu" data-state="false">
<div id="menu_wrapper">
<nav>
<ul>
<li><a data-id="what" title="What We Do" alt="What We Do" onClick={this.scrollSection} >What We Do</a></li>
<li><a data-id="why" title="Why We Do It" alt="Why We Do It" onClick={this.scrollSection}>Why We Do It</a></li>
<li><a data-id="experiance" title="Our Experience" alt="Our Experience" onClick={this.scrollSection}>Our Experience</a></li>
<li><a data-id="how" title="How We Do It" alt="How We Do It" onClick={this.scrollSection}>How We Do It</a></li>
<li><a data-id="competence" title="Competence" alt="Competence" onClick={this.scrollSection}>Competence</a></li>
<li><a data-id="contact" title="Contact" alt="Contact" onClick={this.scrollSection}>Contact</a></li>
</ul>
</nav>
</div>
<div onClick={this.closeMenu} id="shadow_blocker"></div>
</div>
);
}
});
ReactDOM.render(
<MainNav />,
document.getElementById('main-nav')
);
The concept of mixins is code reuse. The same code can be used with multiple components.
But you cant set state for the mixins like you did with menuMixin
The mixin once it is attached to a component and used inside a component will be auto binded with the component and the reference to the component(this) will be autobinded with the mixin function by react.
So you cant have state's to be shared from the mixin. But you can have reference to a dom element and so on but not state. And you can also modify a components state through the setState() method which will update properly, but the state should be present in the component and not in the mixin.
Below is the code snippet from a navigation menu example, which is from this blogpost. What I don't understand is the use of self variable and bind method.
I think this refers to MenuExample when it is stored as self, but I don't know how this changed inside <li>?
Also why is that onClick={this.clicked(index)} doesn't work? In this context what does this refers to?
var MenuExample = React.createClass({
getInitialState: function(){
return { focused: 0 };
},
clicked: function(index){
// The click handler will update the state with
// the index of the focused menu entry
this.setState({focused: index});
},
render: function() {
// Here we will read the items property, which was passed
// as an attribute when the component was created
var self = this;
// The map method will loop over the array of menu entries,
// and will return a new array with <li> elements.
return (
<div>
<ul>{ this.props.items.map(function(m, index){
var style = '';
if(this.state.focused == index){
style = 'focused';
}
// Notice the use of the bind() method. It makes the
// index available to the clicked function:
return <li className={style} onClick={self.clicked.bind(self, index)}>{m}</li>;
}) }
</ul>
<p>Selected: {this.props.items[this.state.focused]}</p>
</div>
);
}
});
// Render the menu component on the page, and pass an array with menu options
ReactDOM.render(
<MenuExample items={ ['Home', 'Services', 'About', 'Contact us'] } />,
document.getElementById('container')
);
onClick={this.clicked(index)} doesn't work because onClick expects a function, not a result of it. Normally, in a click handler, an event will be passed to it, but here they are using bind() to override that behavior and pass the component and the index as 'this' and the first argument.
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/bind
I have a basic ng-repeat where task is sortable
<div ng-sortable="sortableOptions">
<div ng-repeat="task in todo">
{{task}}
...
when a task is draged and droped, it calls $scope.sortableOptions in my controller. I'm wondering if anyone knows how I could pass the task object into that? Could I set it up as a function or..
Basically, I want to pass the task in and update one of the properties inherent to todo.
$scope.sortableOptions2 = {
stop: function(event, ui) {
// do something with the specific todo here
});
}
thank u
If you are using ng-sortable Yeah you can do that ,
<ul data-as-sortable="sortableOptions" data-ng-model="todo">
<li data-ng-repeat="item in todo" data-as-sortable-item>
<div data-as-sortable-item-handle></div>
</li>
</ul>
And in your controller ,
$scope.sortableOptions = {
accept: function (sourceItemHandleScope, destSortableScope) {return boolean}//override to determine drag is allowed or not. default is true.
itemMoved: function (event) {//Do what you want},
orderChanged: function(event) {//Do what you want},
containment: '#board'//optional param.
};
See the documentation here