React Plugin : Building and Publishing React Components to NPM Registry with NWB

 

 

Problem

I have been using multi-select-react in my projects and it is a very useful component on its own.However as the project’s requirement changed, I found that the plugin was missing some of the features needed for my project.

For instance I wanted to have “Select All” and “Select None” options to be shown in the dropdown.

There are other components in NPN which prove this functionality but replacing component meant, I have to rewrite and refactor the code, which was not only a waste of time but could also introduce new bugs.

Solution

  • I decided to write a Wrapper Component which would wrap the multi-select-react  .
  • The Wrapper component would receive the props from the parent component and pass it to its child (ie: multi-select-react).
  • Before passing the properties, it would look for “enableSelectAllNone” property, if it set to true, then it would add “Select All” and “Select None” options to the property and passes it to the child.
  • Intercept the “onChange” event from the child and check if “Select All or Select None” is checked, if so enable all of the options or none of it respectively.

The solution worked perfectly, since I am passing all the properties to the child component, all the existing functions of the child are retained and no code refactoring is required.

 

Publishing to NPM Registry (https://www.npmjs.com):

 

I thought it is a useful enough feature to the share it with others and decided to publish to npmjs plugin registry. However I didn’t want to write build, test and deployment scripts from the scratch just for the sake of publishing to npm. While rummaging through the internet, I found a nifty little tool called “insin/nwb”, which allows you to create, build, test and publish a plugin with very minimal configuration.

 

You can find more details on how to use the nwb toolkit here.

The steps are easy to follow, once I created the project, I added my code to “src/index.js”.

import React, { Component } from 'react';
import PropTypes from 'prop-types';
import MultiSelectReact from 'multi-select-react'

const OptionsState = {
  ALLSELECTED:1,
  NONESELECTED:2,
  DEFAULT:0
}

//Returns of All the options or none of the options are selected.
const isAllorNoneSelected = (options) => {

  if(options.length > 0) {
    let allSelected = true;
    let noneSelected = true;
    options.forEach(option => {
        if(option.id !=='enableAll' && option.id !=='enableNone'){
          if(option.value === true )
            noneSelected = false;
  
          if(option.value === false )
            allSelected = false;
        }
      });
  
    if(allSelected) 
      return OptionsState.ALLSELECTED;
  
    if(noneSelected) 
      return OptionsState.NONESELECTED;
  }
  

  return OptionsState.DEFAULT;
}


const MarkAllOrNoneOptions = (choice,optionList) => {
  
  return optionList.map((option)=>{
      option.value = choice;
      return option;
  });
  
}

class MultiSelectReactExt extends Component {

  constructor(props){
    super(props);
    this.options = [];
    this.isAllNoneEnabled = false;
    this.isAllSelected = false;
    this.isNoneSelected = false;
  }

  optionClicked = (optionsList) => {
   
    if(this.isAllNoneEnabled){
      if(!this.isAllSelected && optionsList[0].value){
        return this.props.optionClicked(MarkAllOrNoneOptions(true,optionsList))
      }

      if(!this.isNoneSelected && optionsList[optionsList.length-1].value){
        return this.props.optionClicked(MarkAllOrNoneOptions(false,optionsList))
      }
    }

    this.props.optionClicked(optionsList);
  }
  selectedBadgeClicked = (optionsList) => {
    
    if(this.isAllNoneEnabled){
      if(!this.isAllSelected && optionsList[0].value){
        return this.props.selectedBadgeClicked(MarkAllOrNoneOptions(true,optionsList))
      }

      if(!this.isNoneSelected && optionsList[optionsList.length-1].value){
        return this.props.selectedBadgeClicked(MarkAllOrNoneOptions(false,optionsList))
      }
    }

    this.props.selectedBadgeClicked(optionsList);
  }

  shouldComponentUpdate = (nextProps, _) => {
    if(nextProps.options.length > 0 && nextProps.enableSelectAllNone && nextProps.options[0].id !== 'enableAll'){
      this.isAllNoneEnabled = false;
    }
    return true;
  }
  
  
  render() {

    let {options,enableSelectAllNone} = this.props;
    this.options = options;
    
    
    //Add Select all and Select None Options
    if(!this.isAllNoneEnabled && enableSelectAllNone){
      options = [{'label':'Select All','id':'enableAll','value':false}].concat(options.concat([{'label':'Select None','id':'enableNone','value':false}]));
      this.isAllNoneEnabled = true;
    }
    //Remove Select all and Select None Options
    if(this.isAllNoneEnabled && !enableSelectAllNone)
    {
      this.isAllNoneEnabled = false;
    }

    //mark SelectAll or Select None Options of all other or no other was selected respectively
    if(this.isAllNoneEnabled){
      let selectedOption  = isAllorNoneSelected(options);
      this.isAllSelected  = (selectedOption === OptionsState.ALLSELECTED);
      this.isNoneSelected = (selectedOption === OptionsState.NONESELECTED);
      options[0].value = (selectedOption === OptionsState.ALLSELECTED);
      options[options.length-1].value = (selectedOption === OptionsState.NONESELECTED);
    }
    

    return (
       <MultiSelectReact
        {...this.props}  
        options={options}
        optionClicked={this.optionClicked}
        selectedBadgeClicked={this.selectedBadgeClicked}
       />
    );
  }
}

MultiSelectReactExt.propTypes = {
  enableSelectAllNone:PropTypes.bool
};

export default MultiSelectReactExt;

 

I added the mock up data to the index.js in “demo/src/index.js”

import React, {Component} from 'react'
import {render} from 'react-dom'

import MultiSelectReact from '../../src'

class Demo extends Component {
  constructor() {
        super();
        this.state = {
            multiSelect:[]
        };
    }
    componentWillMount(){
        setTimeout(()=>{
            this.setState({multiSelect: [{'label':'Monkey','id':1,'value':true},{'label':'Donkey','id':2,'value':true},{'label':'Lion','id':3,'value':true},{'label':'Zebra','id':4,'value':true}]});
        },5000)
    }
    render() {
        const selectedOptionsStyles = {
            color: "#3c763d",
            backgroundColor: "#dff0d8"
        };
        const optionsListStyles = {
            backgroundColor: "#fcf8e3",
            color: "#8a6d3b"
        };
    return (
      <MultiSelectReact 
      options={this.state.multiSelect}
      optionClicked={this.optionClicked.bind(this)}
      selectedBadgeClicked={this.selectedBadgeClicked.bind(this)}
      selectedOptionsStyles={selectedOptionsStyles}
      optionsListStyles={optionsListStyles} 
      enableSelectAllNone={true}
      />
    );
    }

    optionClicked(optionsList) {
        this.setState({ multiSelect: optionsList });
    }
    selectedBadgeClicked(optionsList) {
        this.setState({ multiSelect: optionsList });
    }
}

render(<Demo/>, document.querySelector('#demo'))

 

I added my Unit Test Code to “test/index-test.js”, nwb provided “mocha” and “karma” support out of the box!

npm run build

Once I was satisfied with the component, I built my project

npm publish

and published to npm registry.

Links

 

Plugin Repo Link : “https://github.com/lemorian/react-multi-select-ext