0
\$\begingroup\$

A simple tab scroller built with React.js that utilizes jQuery for the animation.

Questions

  1. I've only just started learning React and I've recently come across <MyComponent></MyComponent>, though the information was limited. Where can I go to learn more about this?
  2. Would it be better to use the previous question's way to design this program? It seems, if I understand it correctly, that I'm essentially doing the same thing, but the way I've done it doesn't allow for true scalability and isn't the proper way to accomplish this task.

I've created a JSFiddle demo

/**
* Name:        React + jQuery Tab Scroller
* Description: A simple Tab Scroller
* @package     Chimera Apps
* @version     1.0.7
* @author      Chimera.Zen
* @copyright   Copyright (c) 2018, Chimera.Zen
* @link        https://github.com/ChimeraZen
* @license     http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
*/

/*
To be added: 
  -	a means of getting the width of the tab list
  - enclose the list in a container and get displayed width
  - disable TabNav arrow if at start/end of tab list scrollable area
*/

const tabs = [
        {
          id: 0,
          label: "Archery",
          content: "Lorem Ipsum 1"
        },
        {
          id: 1,
          label: "Baseball",
          content: "Lorem Ipsum 2"
        },
        {
          id: 2,
          label: "Basketball",
          content: "Lorem Ipsum 3"
        },
        {
          id: 3,
          label: "Boxing",
          content: "Lorem Ipsum 4"
        },
        {
          id: 4,
          label: "Football",
          content: "Lorem Ipsum 5"
        },
        {
          id: 5,
          label: "Golf",
          content: "Lorem Ipsum 6"
        },
        {
          id: 6,
          label: "Soccer",
          content: "Lorem Ipsum 7"
        },
        {
          id: 7,
          label: "Surfing",
          content: "Lorem Ipsum 8"
        }
      ];

function TabContent(props) {
  return (
    <div className="tabContent">
      {props.content}
    </div>
  );
}

class Tab extends React.Component {
  constructor(props) {
    super(props);
    this.onClick = this.onClick.bind(this);
  }

  onClick(el) {
    this.props.handleClick(el.target)
  }

  render() {
    let active = (this.props.id === this.props.activeTab) ? "active" : ""
    return (
      <li id={this.props.id} onClick={this.onClick} className={active}>
        {this.props.label}
      </li>
    );
  }
}

class TabList extends React.Component {
  constructor(props) {
    super(props);
  }

  componentDidUpdate() {
    $(this.refs.tabList).animate({scrollLeft: this.props.scrollPosition}, 400)
  }

  render() {
    let tabList = this.props.tabs.map((tab) => {
      return (
        <Tab 
          key={tab.id}
          id={tab.id}
          activeTab={this.props.activeTab}
          label={tab.label}
          handleClick={this.props.handleClick}
        />
      );
    });
    return (
      <ul className="tabList" ref="tabList">
        {tabList}
      </ul>
    );
  }
}

class TabNav extends React.Component {
  constructor(props) {
    super(props);
    this.onClick = this.onClick.bind(this);
  }

  onClick(el) {
    this.props.handleClick(el.target)
  }

  render() {
    return (
      <div className="tabNav">
        <i className="fa fa-chevron-left prev" onClick={this.onClick}></i>
        <i className="fa fa-chevron-right next" onClick={this.onClick}></i>
      </div>
    );
  }
}

class TabScroller extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      tabListWidth: 	556,	// Get TabList width, TabList's total width
      scrollDistance: 	100,	// Distance TabList should be scrolled
      scrollPosition: 	0,
      scrollSpeed:      400,	// Transition speed (Time in ms)
      activeTab:        0,		// ID of active Tab
      tabs:             tabs	// Array of objects {id, label, content}
    }
    this.handleNavClick = this.handleNavClick.bind(this)
    this.handleTabClick = this.handleTabClick.bind(this)
  }

  handleNavClick(el) {
    let scrollPosition = this.state.scrollPosition
    let scrollRemaining = this.state.tabListWidth - this.state.scrollPosition

    if ($(el).hasClass("next")) {
      if (scrollRemaining < this.state.scrollDistance) {
        scrollPosition = scrollPosition + scrollRemaining
      } else {
        scrollPosition = scrollPosition + this.state.scrollDistance
      }
    } else {
      if (scrollPosition - this.state.scrollDistance < this.state.scrollDistance) {
        scrollPosition = 0
      } else {
        scrollPosition = scrollPosition - this.state.scrollDistance
      }
    }
    this.setState({scrollPosition: scrollPosition})
  }

  handleTabClick(el) {
    let tabId = parseInt(el.id)
    this.setState({activeTab: tabId})
  }

  render() {
    return (
      <div className="tabScroller">
        <div className="NavList">
          <TabNav handleClick={this.handleNavClick} />
          <TabList 
            tabs={this.state.tabs} 
            activeTab={this.state.activeTab}
            scrollPosition={this.state.scrollPosition} 
            handleClick={this.handleTabClick}
          />
        </div>
        <TabContent content={this.state.tabs[this.state.activeTab].content} />
      </div>
    );
  }
}

// ========================================

ReactDOM.render(
  <TabScroller />,
  document.getElementById('root')
);
* {
  margin: 0;
  padding: 0;
}

#root {
  background: #20262e;
  color: white;
}

.tabScroller {
  display: flex;
  flex-wrap: wrap;
}

.NavList {
  display: flex;
  overflow: hidden;
}

.tabNav {
  display: flex;
  justify-content: center;
  align-items: center;
}

.tabNav i {
  display: flex;
  justify-content: center;
  align-items: center;
  width: 35px;
  height: 35px;
  cursor: pointer;
}

.tabNav i:hover {
  background: #353f4c;
}

ul {
  display: flex;
  align-items: stretch;
  width: 100%;
  list-style-type: none;
  border-left: 1.5px solid #2D333B;
  border-right: 1.5px solid #2D333B;
  overflow: hidden;
}

li {
  display: flex;
  align-items: center;
  padding: 0 10px;
  cursor: pointer;
  transition: all .3s ease-in-out;
}

ul li:last-child {
  margin: 0;
  border-right: none;
}

ul li:hover {
  background: #1566b4;
  transition: all .3s ease-in-out;
}

.active {
  background: #104C86;
}

.tabContent {
  display: flex;
  width: 100%;
  height: 300px;
  padding: 10px;
  border-top: 1.5px solid #2D333B;
}
<html>
  <body>
    <div id="root"></div>
  </body>
</html>

\$\endgroup\$
1
\$\begingroup\$

First Question

I've only just started learning React and I've recently come across <MyComponent></MyComponent>, though the information was limited. Where can I go to learn more about this?

Did you already look at the React documentation for Component? If not, then I would start there. Otherwise feel free to search the web for "react component" and similar queries.

Feedback

Useless constructor override

The TabList overrides the parent constructor and does nothing beyond call the parent constructor. Perhaps it was leftover from when there was more functionality but it would be wise to remove the method, in case the parent method signature was to change, then this method override would also would need to be updated.

constructor(props) {
  super(props);
}

Using const vs let

For values that aren't re-assigned after use, const could be used instead of let. For example, in the render method of Tab, the value for active doesn't get re-assigned - it is merely used in the return statement immediately after being assigned. Some developers default to using const unless re-assignment is mandatory

render() {
    //this value doesn't get re-assigned, so could use const
    let active = (this.props.id === this.props.activeTab) ? "active" : ""
    return (
      <li id={this.props.id} onClick={this.onClick} className={active}>
        {this.props.label}
      </li>
    );
} 

parseInt() without a radix

The click handler calls parseInt() with no second parameter (i.e. radix). The MDN documentation states one should "Always specify this parameter to eliminate reader confusion and to guarantee predictable behavior"3. So that call should have 10 added as the second parameter:

handleTabClick(el) {
    let tabId = parseInt(el.id, 10)

Using {tab.id} as the id attribute

The id attribute should be unique across the entire page, because it "defines a unique identifier (ID) which must be unique in the whole document"1. Consider the scenario where you have another component that displays a list of items on the same page with the Nav tabs. If that component also used the id property of each item in a list, which happened to also be an integer like it is for the tab items, then there could be multiple elements with the same value for that attribute.

Also, the HTML 4.01 specification stated that an id value "must begin with a letter ([A-Za-z]) and may be followed by any number of letters, digits ([0-9]), hyphens ("-"), underscores ("_"), colons (":"), and periods (".")"2. While most restriction has been removed with the HTML 5 specification, it is still wise to adhere to that for compatibility.

The HTML element data attributes could be used here for specifying the tab.id value, and have the id attribute start with a string. For example:

render() {
    //this value doesn't get re-assigned, so could use const
    let active = (this.props.id === this.props.activeTab) ? "active" : ""
    return (
      <li id={'tab_' + this.props.id} data-tab-id={this.props.id} onClick={this.onClick} className={active}>
        {this.props.label}
      </li>
    );
} 

Then the click handler method could utilize jQuery's .data() method to fetch that value:

handleTabClick(el) {
    let tabId = parseInt($(el).data('tabId'), 10);
    this.setState({activeTab: tabId})
}

Event delegate instead of using click handlers as properties

Instead of passing click handlers to each component, a single click handler can be used on the TabScroller component. Then in the click handler, check to see if the target of the event matches an element in the TabNav component or in the TabList component (utilizing .is()), and call the appropriate method:

onClick(event) {
  const target = $(event.target);
  if (target.is(".tabNav i")) {
    this.handleNavClick(target);
  }
  else if (target.is('.tabList li')) {
    this.handleTabClick(target);
  }
}

This allows removing some of those empty constructor overrides. See this in the demonstration snippet below:

const tabs = [
				{
        	id: 0,
          label: "Archery",
          content: "Lorem Ipsum 1"
        },
				{
        	id: 1,
          label: "Baseball",
          content: "Lorem Ipsum 2"
        },
				{
        	id: 2,
          label: "Basketball",
          content: "Lorem Ipsum 3"
        },
				{
        	id: 3,
          label: "Boxing",
          content: "Lorem Ipsum 4"
        },
				{
        	id: 4,
          label: "Football",
          content: "Lorem Ipsum 5"
        },
				{
        	id: 5,
          label: "Golf",
          content: "Lorem Ipsum 6"
        },
				{
        	id: 6,
          label: "Soccer",
          content: "Lorem Ipsum 7"
        },
				{
        	id: 7,
          label: "Surfing",
          content: "Lorem Ipsum 8"
        }
      ];

function TabContent(props) {
  return (
    <div className="tabContent">
    {props.content}
    </div>
  );
}

class Tab extends React.Component {
  
  render() {
  	const active = (this.props.id === this.props.activeTab) ? "active" : ""
    return (
      <li id={'tab_' + this.props.id} data-tab-id={this.props.id} className={active}>
        {this.props.label}
      </li>
    );
  }
}

class TabList extends React.Component {
  
  componentDidUpdate() {
    $(this.refs.tabList).animate({scrollLeft: this.props.scrollPosition}, 400)
  }
  
  render() {
  	let tabList = this.props.tabs.map((tab) => {
    	return (
      	<Tab 
          key={tab.id}
          id={tab.id}
          activeTab={this.props.activeTab}
          label={tab.label}
        />
      );
    });
    return (
    	<ul className="tabList" ref="tabList">
        {tabList}
      </ul>
    );
  }
}

class TabNav extends React.Component {  
  render() {
    return (
    	<div className="tabNav">
  		  <i className="fa fa-chevron-left prev"></i>
        <i className="fa fa-chevron-right next"></i>
      </div>
    );
  }
}

class TabScroller extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      tabListWidth: 		556,	// Get TabList width, TabList's total width
      scrollDistance: 	100,	// Distance TabList should be scrolled
      scrollPosition: 	0,
      scrollSpeed:			400,	// Transition speed (Time in ms)
      activeTab:				0,		// ID of active Tab
      tabs:							tabs	// Array of objects {id, label, content}
    }
    this.handleNavClick = this.handleNavClick.bind(this)
    this.handleTabClick = this.handleTabClick.bind(this)
  }
  onClick(event) {
    const target = $(event.target);
  	if (target.is(".tabNav i")) {
    	this.handleNavClick(target);
    }
    else if (target.is('.tabList li')) {
    	this.handleTabClick(target);
    }
  }
  handleNavClick(el) {
    let scrollPosition = this.state.scrollPosition
    let scrollRemaining = this.state.tabListWidth - this.state.scrollPosition
    
    if ($(el).hasClass("next")) {
    	if (scrollRemaining < this.state.scrollDistance) {
      	scrollPosition = scrollPosition + scrollRemaining
      } else {
    		scrollPosition = scrollPosition + this.state.scrollDistance
      }
    } else {
    	if (scrollPosition - this.state.scrollDistance < this.state.scrollDistance) {
      	scrollPosition = 0
      } else {
    		scrollPosition = scrollPosition - this.state.scrollDistance
      }
    }
  	this.setState({scrollPosition: scrollPosition})
  }
  
  handleTabClick(el) {
  	let tabId = parseInt($(el).data('tabId'), 10);
  	this.setState({activeTab: tabId})
  }
  
  render() {
    return (
      <div className="tabScroller" onClick={this.onClick.bind(this)}>
        <div className="NavList">
          <TabNav />
          <TabList 
            tabs={this.state.tabs} 
            activeTab={this.state.activeTab}
            scrollPosition={this.state.scrollPosition}
          />
        </div>
        <TabContent content={this.state.tabs[this.state.activeTab].content} />
      </div>
    );
  }
}

// ========================================

ReactDOM.render(
  <TabScroller />,
  document.getElementById('root')
);
* {
  margin: 0;
  padding: 0;
}

#root {
  background: #20262e;
  color: white;
}
.tabScroller,
.NavList,
.tabNav,
.tabNav i,
ul,
li,
.tabContent {
  display: flex;
}
.tabScroller {
  flex-wrap: wrap;
}

.NavList {
  overflow: hidden;
}

.tabNav {
  justify-content: center;
  align-items: center;
}

.tabNav i {
  justify-content: center;
  align-items: center;
  width: 35px;
  height: 35px;
  cursor: pointer;
}

.tabNav i:hover {
  background: #353f4c;
}

ul {
  align-items: stretch;
  width: 100%;
  list-style-type: none;
  border-left: 1.5px solid #2D333B;
  border-right: 1.5px solid #2D333B;
  overflow: hidden;
}

li {
  align-items: center;
  padding: 0 10px;
  cursor: pointer;
  transition: all .3s ease-in-out;
}

ul li:last-child {
  margin: 0;
  border-right: none;
}

ul li:hover {
  background: #1566b4;
  transition: all .3s ease-in-out;
}

.active {
  background: #104C86;
}

.tabContent {
  width: 100%;
  height: 300px;
  padding: 10px;
  border-top: 1.5px solid #2D333B;
}
<link href="https://maxcdn.bootstrapcdn.com/font-awesome/4.2.0/css/font-awesome.min.css" rel="stylesheet"/>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<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>

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.