Why does Ant Designs Tree component render so slow when expanding? - javascript

I'm building an React component/EntityBrowser that holds a Tree component from ant.design. It's a prototype going into production soon. It's still based on create-react-app. State is managed through Redux.
I've tried searching for answers using both google and Stackoverflow. The consensus seems to be; whenever the Tree components treeData prop receives too much data, it slows down the rendering process, because each of the TreeNodes needs to be re-rendered every time the Tree is expanded. Since more data means slower rendering for each of the TreeNodes - well.. adding more TreeNodes also means slower rendering by the factor the extra data adds.
I don't know if this is the real reason for the overall performance. I've profiled the component and indeed adding more data results in worse rendering times and same goes for the amount of nodes. I would expect this. I'm just surprised by the rate the Tree performance, both when adding new nodes dynamically, but also when expanding it, deteriorates. It does not require many nodes. After only a few nodes with minimal data, it's already too slow to be usable.
This is the Tree component:
<Tree
rootClassName={dark ? "dark-tree" : ""}
style={{ borderRadius: "0" }}
blockNode={true}
onExpand={onExpand}
onSelect={onSelect}
selectedKeys={selectedKeys}
expandedKeys={expandedKeys}
treeData={treeData}
titleRender={TreeNode}
/>
These are the props handling onExpand and onSelect.
const onExpand = (expandedKeys) => {
dispatch(entityBrowserActions.updateExpandedKeys(expandedKeys));
};
const onSelect = (selectedKeys, { event }) => {
if (handleSelection) handleSelection(selectedKeys);
dispatch(entityBrowserActions.updateSelectedKeys(selectedKeys));
};
The handleSelection call tells the app to show the right "scenario" based on the current entityStructure. We are choosing between them in the EntityBrowser/Tree itself.
The treeData is generated with:
const createTreeData = (entityStructure) => {
return entityStructure.map((item) => {
const itemSelectable = !selectedKeys.includes(item.key);
let title = (<span>{item.name}</span>);
let itemObj = {};
let newName = (e) => onNameChange(e, item.key);
let stopEdit = () => handleStopEdit(item.key);
if (editable && editingEntities[item.key] == true) {
title = (
<Input
value={item.name}
onChange={ newName }
onBlur={ stopEdit }
/>
);
}
if (item.children) {
itemObj = {
title,
key: item.key,
selectable: itemSelectable, // Selectable only if not already selected
checkable: false,
children: createTreeData(item.children),
};
} else {
itemObj = {
title,
key: item.key,
selectable: itemSelectable,
}
}
return itemObj;
});
};
The "design" of the treenodes is added with the titleRender prop:
const TreeNode = (value, record) => {
const handleEdit = () => handleStartEdit(value.key);
const handleClose = () => handleClosePopup(value.key, deletePopoverVisible[value.key]);
const handleDeleteAndClose = () => {
handleDelete(value.key);
handleClosePopup(value.key, false);
}
return (
<div className="tree-title">
{value.title}
{editable ? (
<div style={{}}>
<Button
type="text"
style={{ padding: "2px 4px" }}
onClick={ handleEdit }>
<EditOutlined style={{ fontSize: "1.1rem" }} />
</Button>
<Popover
visible={ deletePopoverVisible[value.key] }
onVisibleChange={ handleClose }
content={
<div style={{ display: "flex", flexDirection: "column" }}>
<Button
type="text"
style={{ color: "red" }}
onClick={ handleDeleteAndClose }>
Delete
</Button>
<Button
type="text"
onClick={ handleClose }>
Cancel
</Button>
</div>
}
trigger="click">
<Button type="text" style={{ padding: "2px 4px" }}>
<DeleteOutlined style={{ fontSize: "1.1rem" }} />
</Button>
</Popover>
</div>
) : null}
</div>
);
};
I've tried to remove all the "unnecessary" props like onSelect, titleRender, blockNode, etc. When doing this it does give a speed up but even if I remove everything but the bare minimum it is still painfully slow. Initially all the function calls happened on props inline and now I've "pulled them out" to try and gain some speed. Got a little bit - but not nearly enough.
I don't know how to proceed here. I'm hoping someone can point me to something super obvious that I'm missing.
Everything and nothing worked.

Related

FAQ's section individual toggle incorrectly opening all parts on click

I've been trying to do a sort of toggle whereby you are able to click on a question to expand the answer. I've tried adapting the code from https://codesandbox.io/s/polished-rain-xnez0?file=/src/App.js, which was from another question on here.
Instead of creating a map in the parent component and passing in them separately to a reusable 'Expandable' component to render separate functional components as shown in the example, I tried creating the map within the FAQ component:
FAQ Expandable Component:
const FAQ = ({ questions }) => {
const [expanded, setExpanded] = useState(false);
const handleClick = () => {
setExpanded((prevExpanded) => !prevExpanded);
};
const renderedQuestions = questions.map((question, index) => {
return (
<React.Fragment key={question.id}>
<FAQIndividualWrapper>
<FAQTitle
className='title'
onClick={() => handleClick()}
>
{/* <i></i> */}
{question.title}
</FAQTitle>
<FAQContent className='content' style={{ display: expanded ? "block" : "none" }}>
{question.content}
</FAQContent>
</FAQIndividualWrapper>
</React.Fragment>
)
})
return (
<>
{renderedQuestions}
</>
)
Parent Component:
const questions = [
{
id: 1,
title: 'Question 1',
content: 'Answer 1'
},
{
id: 2,
title: 'Question 2',
content: 'Answer 2'
}
]
const FAQSection = () => {
return (
<FAQPageContainer>
<FAQWrapper>
<FAQ questions={questions} />
</FAQWrapper>
</FAQPageContainer>
)
}
However, my code results in all the answers being expanded on any click of either question. Why is this happening?
Also, how should I structure and fix the code for 'ideal' programming?
Thank you!
The problem with your code is that you only have one expanded state for all questions. This means the open/collapse state is the same for all questions.
If you want to include all of your code inside one component. You need to somehow distinguish the open/collapse state of each child.
Here I'm using an array to store individual open/collapse state of each child.
// initial state is an array with the length of your questions array, default to false
const [expandedIndexes, setExpandedIndexes] = useState(
Array(info.length).fill(false)
);
const handleClick = (index) => {
setExpandedIndexes((prevExpandedIndexes) => {
const newState = [...prevExpandedIndexes];
// set state for the corresponding index
newState.splice(index, 1, !prevExpandedIndexes[index]);
return newState;
});
};
return (
<div className="details">
{info.map(({ title, details, id }, index) => (
<div key={id} className="details-wrapper">
<div>
<h3 className="title">{title}</h3>
<button onClick={() => handleClick(index)}>+</button>
</div>
<p
className="text"
// check the corresponding state to display
style={{ display: expandedIndexes[index] ? "block" : "none" }}
>
{details}
</p>
</div>
))}
</div>
);
Codesandbox
Also, how should I structure and fix the code for 'ideal' programming?
Ideally, you want to follow the convention in your codesandbox link above. That way you don't need to deal with this kind of logic, each child will have its own state/handleClick function.
It is correct that the issue is caused by the fact that you've only a single boolean state.
Use an object to store the ids of the questions that are expanded, toggling a boolean value for each.
Example:
const FAQ = ({ questions }) => {
const [expanded, setExpanded] = useState({});
// Curried callback to enclose the id in callback scope
const handleClick = id => () => {
setExpanded(expanded => ({
...expanded,
[id]: !expanded[id],
}));
};
return questions.map((question) => (
<FAQIndividualWrapper key={question.id}>
<FAQTitle
className='title'
onClick={handleClick(question.id)}
>
{question.title}
</FAQTitle>
<FAQContent
className='content'
style={{ display: expanded[question.id] ? "block" : "none" }}
>
{question.content}
</FAQContent>
</FAQIndividualWrapper>
));
}

Tree items dissapearing on scroll down with react-virtualized

recently I have been using react-virtualized library to render my tree item view. I have followed example from the docs however I end up having very strange problem with items disappearing when I scroll down.
I have created codesandbox to show this behaviour and code.
https://codesandbox.io/s/bitter-snow-23vci?file=/src/App.js
Main idea of virtualized list to render it as a list.
If you pass down tree like structure and render it like in your code sample
<List
....
rowCount={data.length}
/>
You don't change rowCount value and keep expanded state in your Node component.
const Node = ({ data, listRef, depth }) => {
const [isExpanded, setIsExpanded] = React.useState(false);
But then you scroll out of screen your Node element will be destroyed and recreated then you return.
You need to keep your selections outside of Node element.
like
// [key]: value structure there key is id of element and value [true, false].
const rootObject = {[elementId]: true};
const App = () => {
const [visibleNodes, setVisibleNodes] = useState(rootObject)
....
<List
...
rowRenderer={({ index, style, key }) => {
return (
<Node
setVisibleNodes={setVisibleNodes}
visibleNodes={visibleNodes}
style={style}
key={key}
data={data[index]}
listRef={ref}
depth={1}
/>
);
}}
rowCount={data.length}
width={width}
/>
And in Node
const Node = ({ data, listRef, depth, setVisibleNodes, visibleNodes }) => {
const isExpanded = visibleNodes[data.id];
const handleClick = (e) => {
if (data.children.length === 0) return;
e.stopPropagation();
setVisibleNodes({...visibleNodes, [data.id]: !!isExpanded});
listRef.current.recomputeRowHeights();
listRef.current.forceUpdate();
};
return (
<div onClick={handleClick}>
{data.children.length ? (isExpanded ? "[-]" : "[+]") : ""} {data.name}
{isExpanded && (
<div style={{ marginLeft: depth * 15 }}>
{data.children.map((child, index) => (
<Node
key={index}
data={child}
listRef={listRef}
depth={depth + 1}
/>
))}
</div>
)}
</div>
);
};
I think it works)
But it's better to do such things like real list and make tree hierarchy just visually. By that way you'll use Virtualisation List as it was purposed by creators)

Access to DOM elements by ID in React

I want to handle disable attribute of input elements by ID in my React app
It's possible with states but number of elements is not fixed, maybe 10 input or 20 or more ...
I've decided to set ID for each input and access to them with ID, for example :
document.getElementById('title-12') ....
So, is it a suitable trick or best practice to handle this issue ?
Performance and clean code is very important for me :-)
Thanks
Oops... my bad. I digged into your discussion and here's a new solution.
That's still correct that React approach is advised so we should use reusable components for inputs (we can have any number of inputs right now). All the input's data are stored in parent component's store as a collection. We map through collection and send properties to each component (in the simplest version - id, isDisabled and disableInput() function).
class Titles extends Component {
constructor(props) {
super(props);
this.state = {
titles: [
{
id: 0,
disabled: true
},
{
id: 1,
disabled: false
}
]
};
}
addNewInput = () => {
const prevList = this.state.titles;
const newItem = {
id: prevList.length,
disabled: false
};
this.setState({ titles: [...prevList, newItem] });
};
disableInput = id => {
const titles = this.state.titles;
titles[id].disabled = !titles[id].disabled;
this.setState({ titles });
};
render() {
return (
<div>
<h1>Titles list</h1>
<form style={{ display: "flex", flexDirection: "column" }}>
{this.state.titles.map(title => (
<Title
key={title.id}
id={title.id}
isDisabled={title.disabled}
disableInput={id => this.disableInput(id)}
/>
))}
</form>
<button onClick={() => this.addNewInput()}>Dodaj nowy</button>
</div>
);
}
}
in Title component we just render the props in to <input> and a button with onClick function that sends id of this component to its parent, where disable attribute's value is being reversed.
const Title = ({ id, isDisabled, disableInput }) => {
return (
<div>
<input
id={id}
type="text"
placeholder={id}
disabled={isDisabled}
/>
<button type="button" onClick={() => disableInput(id)}>
disable input
</button>
</div>
);
};
Working example can be found here.
Please let me know if it works for you.

React removing an element when onClick

I am trying to remove a div when onClick is pressed. The div exists on my parent component where I have
render() {
const listPlayers = players.map(player => (
<Counter
key={player.id}
player={player}
name={player.name}
sortableGroupDecorator={this.sortableGroupDecorator}
decrementCountTotal={this.decrementCountTotal}
incrementCountTotal={this.incrementCountTotal}
removePlayer={this.removePlayer}
handleClick={player}
/>
));
return (
<ContainLeft style={{ alignItems: 'center' }}>
<ProjectTitle>Score Keeper</ProjectTitle>
<Copy>
A sortable list of players that with adjustable scores. Warning, don't go negative!
</Copy>
<div>
<Stats totalScore={this.state.totalScore} players={players} />
{listPlayers}
</div>
</ContainLeft>
);
}
It passes props to the child component where the button to delete the div, here
return (
<div
style={{ display: this.state.displayInfo }}
className="group-list"
ref={sortableGroupDecorator}
id="cell"
>
<CountCell style={{ background: this.state.color }}>
<Row style={{ alignItems: 'center', marginLeft: '-42px' }}>
<Col>
<DeleteButton onClick={removePlayer}>
<Icon name="delete" className="delete-adjust fa-minus-circle" />
</DeleteButton>
</Col>
(I snipped the rest of the code because it was long and not useful here)
The array (a separate file) is imported into the Parent component and it reads like this
const players = [
{
name: 'Jabba',
score: 10,
id: 11
},
{
name: 'Han',
score: 10,
id: 1
},
{
name: 'Rey',
score: 30,
id: 10
}
];
export default players;
So what I'm trying to do is write a function on the main parent that when it is clicked inside the child, the div is removed, deleted, gone (whatever the best term is) sort of like "remove player, add player"
On my parent component, I've written a function where the console.log works when it is clicked in the child, but whatever I write in the function doesn't seem to want to work.
The function I'm building (in progress, I'm still a little lost here) is:
removePlayer() {
console.log('this was removed');
players.splice(2, 0, 'Luke', 'Vader');
}
which is mapped over here as a prop
const listPlayers = players.map(player => (
<Counter
key={player.id}
player={player}
name={player.name}
sortableGroupDecorator={this.sortableGroupDecorator}
decrementCountTotal={this.decrementCountTotal}
incrementCountTotal={this.incrementCountTotal}
removePlayer={this.removePlayer}
handleClick={player}
/>
));
And passed into the child here:
render() {
const {
name,
sortableGroupDecorator,
decrementCountTotal,
incrementCountTotal,
removePlayer
} = this.props;
return (
<div
style={{ display: this.state.displayInfo }}
className="group-list"
ref={sortableGroupDecorator}
id="cell"
>
<CountCell style={{ background: this.state.color }}>
<Row style={{ alignItems: 'center', marginLeft: '-42px' }}>
<Col>
<DeleteButton onClick={removePlayer}>
<Icon name="delete" className="delete-adjust fa-minus-circle" />
</DeleteButton>
I know all this is lengthy and I wanted to provide as much detail as I could because React is still new to me and I get confused with some of the verbiages. Thanks for helping out in advance
We sorted it out in chat. Like expected, it was a problem with the state.
I made a small semi-pseudo snippet with comments as explanation:
import React, { Component } from 'react';
// Your player constant, outside the scope of any React component
// This pretty much just lives in your browser as a plain object.
const players = [
{
name: 'Jabba',
score: 10,
id: 11
},
{
name: 'Han',
score: 10,
id: 1
},
{
name: 'Rey',
score: 30,
id: 10
}
];
class App extends Component {
constructor() {
super();
this.state = {
players, // ES6 Syntax, same as players: players
// Add all your other stuff here
};
}
removePlayer(id) {
const newState = this.state;
const index = newState.players.findIndex(a => a.id === id);
if (index === -1) return;
newState.players.splice(index, 1);
this.setState(newState); // This will update the state and trigger a rerender of the components
}
render() {
const listPlayers = this.state.players.map(player => { // Note the this.state, this is important for React to see changes in the data and thus rerender the Component
<Counter
..
removePlayer={this.removePlayer.bind(this)} //bind this to stay in the context of the parent component
/>
});
return (
<div>
{listPlayers}
</div>
);
}
}
//////////////////////////////////////// Child component
....
<DeleteButton onClick={() => this.props.removePlayer(this.props.player.id)}>
....
Here is how i solved this
i've added an id to the element
and then with function remove i detroyed it
const closenotif = () => document.getElementById("notif").remove()
<div id="notif">
<button onClick={destroy}> close </button>
</div>
NB : the element is destroyed in the current document
so in the current render
on Next JS this works perfectly
if you are using a live rendering with react this probably won't work
i'll suggest you to work with states in that case
Little confused about how the whole app works, but I will try to help you.
To make react changes to the dom, you have to put players in the state. So, in the removePlayer you make a copy of this.state.players in a local variable (just to not change the array directly in state, it's a good practice), then you make the split in this local variable and finally you setState({ players: localPlayers}).
This way the "div" will be removed.

React Higher Order Elements - how far can one go with passing props

am refactorizing huge project in which
some components look very similar
there s looots of files (very high level of components vs files granulation)
I've thought and searched for ways to handle this issue here and there and found this great article about Higher Order Components (HOC) - basically being components that wrap another components.
https://medium.com/#franleplant/react-higher-order-components-in-depth-cf9032ee6c3e#.8vr464t20
I will now give you (A) example of two out of eight similar components types that I need to handle, than (B) I will paste the code I came up with that unifies those eight files into one. Finally (C) will paste example of usage of that unified component.
I will try to be consistent and naming will be not domain driven (I can't post project details here) but same names in different code excerpts below will always point to same components and data. Otherwise I will point it.
(A) - similar components types
1. TabA - Simple one
export default class TabA extends Component {
render() {
return (
<PageWrapper>
<Grid>
<GridItem xsSize="3">
<SmartComponent something={this.props.something }/>
</GridItem>
<GridItem xsSize="9">
<Tabs
permalink = { this.props.permalink }
history={ this.props.history }
activeTab={ Paths.somePath }
/>
<TabAContent
data={ this.props.data }
name={ this.props.name }
someValue={ this.props.someValue }
/>
</GridItem>
</Grid>
</PageWrapper>
);
}
}
Notice that SomeComponentA does not take any children in. Also there is no conditional rendering of any kind here.
2. TabB - More complex one
Similarly here, notice that renderSomeData method conditionally renders SmartComponentToBeConditionallyRendered and also SomeComponentB takes children in from the props.
export default class TabB extends Component {
renderSomeData() {
let someData = {
header: "Header text",
searchPlaceHolder: 'Search (name)',
buttonCaption: 'button caption'
};
return (
<SmartComponentToBeConditionallyRendered
type={ 'some_type' }
permalink={ this.props.permalink }
data={ someData }
/>
)
}
render() {
let { data } = this.props;
return (
<div>
<PageWrapper>
<Grid>
<GridItem xsSize="3">
<SmartComponent something={this.props.something}/>
</GridItem>
<GridItem xsSize="9">
<Tabs
permalink = { this.props.permalink }
history = { this.props.history }
activeTab = { Paths.somePage }
/>
<TabBContent data = { data }>
{this.props.children}
</TabBContent>
</GridItem>
</Grid>
</PageWrapper>
{
this.context.hasPermission('somePermission') ?
this.renderSomeData() :
null
}
</div>
)
}
static contextTypes = {
hasPermission: React.PropTypes.func.isRequired
}
}
Those eight components I've wrote about at the beginning - they all represent one of three possibilities.
Two pictured above and possibility C, but differences in C are just another conditionally rendered components so not worth mentioning cause it will finally come down to passing more flags in props.
Those two components above - they differ in:
kind of SomeComponentX - in place of X there may A, B but also C, D, E etc. in every of those eight similar components. Each of SomeComponentX take in different props as well.
Paths.VALUE_HERE
if SomeComponentX takes in any children or not
if they conditionally render data from renderSomeData method
if YES - someData defined inside the method varies as well
permalink
some_type
(B) What I came up with
let availablePartials = {
PartialA: PartialA,
PartialB: PartialB,
PartialC: PartialC
}
export default class GenericTab extends Component {
renderSomeData() {
return (
<SomeData
type = { this.props.type }
permalink = { this.props.permalink }
data = { this.props.someData } //PASSED FROM PROPS NOW
/>
);
}
render() {
let tabContent = React.createElement(
availablePartials[this.props.partialView.name],
this.props.partialView.props,
this.props.renderChildren ? this.props.children : null
);
return (
<div>
<PageWrapper>
<Grid>
<GridItem xsSize="3">
<SmartComponent something = { this.props.permalink }/>
</GridItem>
<GridItem xsSize = "9">
<Tabs
permalink = { this.props.permalink }
history = { this.props.history }
activeTab = { this.props.activeTab }
/>
{ tabContent }
</GridItem>
</Grid>
</PageWrapper>
{
this.context.hasPermission(this.props.requiredPermission) && this.props.dataForSomeDataMethod ?
this.renderSomeData()
: null
}
</div>
)
}
static contextTypes = {
hasPermission: React.PropTypes.func.isRequired
}
};
CityPageTab.propTypes = {
permalink: PropTypes.string,
dataForSomeDataMethod: PropTypes.object,
type: PropTypes.string,
activeTab: PropTypes.string,
renderChildren: PropTypes.bool,
partialView: PropTypes.object,
requiredPermission: PropTypes.string
};
Basically EVERYTHING is constructed from props. The only part I don't like is availablePartials[this.props.partialView.name].
It requires developers to keep the state of availablePartials object consistent and tangles it a bit. Not nice solution but still it is best I came up with so far.
(C) New GenericTab usage example
componentThatUseGenericTabRenderMethod() {
let { valueA, valueB, valueC, history } = this.props;
let someData = {
header: 'header text',
searchPlaceHolder: 'Search (name)',
buttonCaption: 'buttonCaption'
}
return (
<GenericTab
partialView = {{
name: 'PartialA',
props: {
A: valueA,
B: valueB,
C: valueC,
history: history,
permalink: this.props.params.permalink
}
}}
permalink = { this.props.params.permalink }
activeTab = { Paths.somePath }
someData = { someData }
type = { 'SOME_TYPE' }
renderChildren = { false }
requiredPermission = { 'some_required_permision' }
/>
);
}
So that is that. Usage got bit more complex, but I got rid of seven files (and getting rid of files is main objective as there is too many of them) and am going to further push it in similar manner - generic one.
Thing with genericity - it is more difficult to use but saves lots of space.
Project utilises Redux so dont be too concerned about passing props down the tree. They always only come from some SmartParentComponent that renders GenericTab
Below is the visualisation of how it looks on the page.
GenericTab is responsible for rendering Tabs and TabContent parts.
Yes I know it is shitty solution, but am not responsible for architecture of it. There are so many things to be refactorized here and what am asking about is just a step in a journey. So please lets focus on the question asked and not other things that are so wrong with this code. I know.:)
Guess I could make an article out of it but I don't really have blog to do it:).
Please tell me what you think, propose upgrades, different ways of handling this problem etc.
When dealing with such architecture (i.e, tabs in your case), you basically don't want to hide the architecture under the hood, because in this case your ending up adding more and more properties with each new case you want to handle.
Instead, you wan't to let react handles the nested structure since it's where react really shines. That let you write something very generic by handling the built in children props. You typically want to write something like :
const PageWithTabs = (props) => (
<Tabs defaultActive={'targaryens-panel'}>
<TabBar>
<TabLink href="#starks-panel">{'Starks'}</TabLink>
<TabLink href="#lannisters-panel">{'Lannisters'}</TabLink>
<TabLink href="#targaryens-panel">{'Targaryens'}</TabLink>
</TabBar>
<TabPanel id="starks-panel">
<ul style={{ listStyleType: 'none' }}>
<li>Eddard</li>
<li>Catelyn</li>
<li>Robb</li>
<li>Sansa</li>
<li>Brandon</li>
<li>Arya</li>
<li>Rickon</li>
</ul>
</TabPanel>
<TabPanel id="lannisters-panel">
<ul style={{ listStyleType: 'none' }}>
<li>Tywin</li>
<li>Cersei</li>
<li>Jamie</li>
<li>Tyrion</li>
</ul>
</TabPanel>
<TabPanel id="targaryens-panel">
<ul style={{ listStyleType: 'none' }}>
<li>Viserys</li>
<li>Daenerys</li>
</ul>
</TabPanel>
</Tabs>
)
The point here is that you don't have to "predict" all the things that might appear under each TabPanel, simply let the developper put whatever he wants ! BUT me need some logic to handle the "go to tab" sort of things.
React provides some very handy utilities methods to dynamically clone elements, map over elements, and render element whether its type is the one you expect or not (in our case, we expect TabBar or TabPanel type, nothing prevent the developper to put any other components than this two but nothing prevent him neither to put any built in <table> html element inside of <a> tag or something weird like that).
Here is a little implementation with Material Design Lite, it's not perfect but you should get the point :
class Tabs extends React.Component {
constructor(props) {
super(props),
this.state = {
activeTabId: props.defaultActive
}
}
tabClickHandlerFactory(id) {
return (e) => {
e.preventDefault()
this.setState({
activeTabId: id
})
}
}
getPanelIdFromLink(href) {
return href.split('#')[1]
}
render() {
const self = this
return (
<div className='mdl-tabs is-upgraded' {...self.props}>
{React.Children.map(self.props.children, (child, index) => {
if (child.type == TabBar) {
return React.cloneElement(child, {}, React.Children.map(child.props.children, (link) => {
const id = self.getPanelIdFromLink(link.props.href)
return (
React.cloneElement(link, {
onClick: self.tabClickHandlerFactory(id),
active: self.state.activeTabId === id
})
)
}))
}
if (child.type == TabPanel) {
const { id } = child.props
const active = self.state.activeTabId === id
return active && React.cloneElement(child, { active: true })
}
})}
</div>
)
}
}
Tabs.propTypes = {
defaultActive: React.PropTypes.string.isRequired,
}
const TabBar = (props) => <div className='mdl-tabs__tab-bar' {...props}>{props.children}</div>
const TabLink = ({ active, ...props }) => {
return (
<a className={`mdl-tabs__tab${active ? ' is-active' : ''}`} {...props}>{props.children}</a>
)
}
const TabPanel = ({ active, ...props }) => (
<div className={`mdl-tabs__panel${active ? ' is-active' : ''}`} {...props}>{props.children}</div>
)
const PageWithTabs = (props) => (
<Tabs defaultActive={'targaryens-panel'}>
<TabBar>
<TabLink href="#starks-panel">{'Starks'}</TabLink>
<TabLink href="#lannisters-panel">{'Lannisters'}</TabLink>
<TabLink href="#targaryens-panel">{'Targaryens'}</TabLink>
</TabBar>
<TabPanel id="starks-panel">
<ul style={{ listStyleType: 'none' }}>
<li>Eddard</li>
<li>Catelyn</li>
<li>Robb</li>
<li>Sansa</li>
<li>Brandon</li>
<li>Arya</li>
<li>Rickon</li>
</ul>
</TabPanel>
<TabPanel id="lannisters-panel">
<ul style={{ listStyleType: 'none' }}>
<li>Tywin</li>
<li>Cersei</li>
<li>Jamie</li>
<li>Tyrion</li>
</ul>
</TabPanel>
<TabPanel id="targaryens-panel">
<ul style={{ listStyleType: 'none' }}>
<li>Viserys</li>
<li>Daenerys</li>
</ul>
</TabPanel>
</Tabs>
)
ReactDOM.render(<PageWithTabs/>, document.getElementById('app'))
<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>
<link rel="stylesheet" href="https://code.getmdl.io/1.1.3/material.brown-orange.min.css" />
<div id='app'></div>

Categories

Resources