Skip to main content
Kinetic Community

Refactor a stateless component to be stateful

Overview

It can be very difficult to build an application without Redux state. The alternative to using Redux state would be to pass state from component to component. The activity in this article will demonstrate moving state from parent to child.

At the end of the activity there will be an element that when clicked will display a dialog. Onclick events will trigger methods to change the visibility of the dialog. The components state will control the visibility of the dialog.

The component needs to be aware of it's state and have it's own methods. This requires that the component be a statefull (smart) component.

Goals of the activity:

  • Create ServiceCardBottom.js file.
  • Refactor ServiceCardBottom component to be "smart."
  •  Add a question mark icon to ServiceCardBottom.
  • Create ActiveHelp component.
  • Add new scss style sheet and include it into the master.scss file.
  • Add a on click method to open and close dialog.
  • Add help to the form model.
  • Add help to the constants.
  • Create an attribute definition on the Kapp.
  • Add the attribute to a form.
  • Make the display of the question mark icon conditional.

Notes on the Activity: This will be the more difficult than the last activity.  Not all the task have code provided.  Following the comments in the code and remembering how to destructure properties will help you complete this activity.

Activity 6

Move ServiceCardBottom to its own file

  1. From the root of the bundle navigate to the Services directory: <root>/src/components/Services.
  2. Create ServiceCardBottom.js file in the Services directory.
  3. Open ServiceCard.js and add an import for ServiceCardBottom component. 
    import { ServiceCardBottom } from './ServiceCardBottom';
  4. Remove the Link import on line 2.
  5. Remove the ServiceCardBottom component from the ServiceCard.js file.
  6. Open the ServiceCardBottom.js file.
  7. Add the React import to first line of the ServiceCardBottom.js file.  
    import React from 'react';
  8. Add the Link import to the second line of the ServiceCardBottom.js file.   
    import { Link } from 'react-router-dom';
  9. Add the ServiceCardBottom component.  Make a note that export has been added to component.
    export const ServiceCardBottom = ({ categorySlug, form }) =>
      <div className="service-details-wrapper">
        <h5 className="ellipsis">
          <Link
            to={
              categorySlug
                ? `/categories/${categorySlug}/${form.slug}`
                : `/forms/${form.slug}`
            }
          >
            {form.name}
          </Link>
        </h5>
        <p className="ellipsis">{form.description}</p>
      </div>;
    
  10. Save both files and check the browser at the #/ route.  The view should be the same as it was before the change.

Refactor ServiceCardBottom component.

  1. Replace import React with new import statement. 
    import React, { Component } from 'react';
  2. Refactor ServiceCardBottom to be a class component. 
    export class ServiceCardBottom extends Component {  // class that extends component
      constructor(props) {
        super(props);
    
        this.state = { // initialize state
          visible: false,
        };
    
        // bind this to handle open method
        // bind this to handle close method
      }
    
      // Add handleOpen method
    
      // Add handleClose method
    
      render() { // render method is required
        const { categorySlug, form } = this.props; // destructure categorySlug and form
        // Destructure properties from the state object
        return ( // JSX returned from the render method
          <div className="service-details-wrapper">
            <h5 className="ellipsis">
              <Link
                to={
                  categorySlug
                    ? `/categories/${categorySlug}/${form.slug}`
                    : `/forms/${form.slug}`
                }
              >
                {form.name}
              </Link>
              <i className="fa fa-question-circle 2x active-help-icon" title="Help" role="button" tabIndex="0" />
            </h5>
            <p className="ellipsis">{form.description}</p>
            {/* Add ActiveHelp component */}
          </div>
        );
      }
    }
    • The ServiceCardBottom has been turned into a class that extends Component
    • The class has a constructor method where initial state is set.
    • Classes must have a render method.
    • In the render method categorySlug and form have been destructured from the props object.
    • The ServiceCardBottom JSX is being returned from the render method.
  3. Save the file and view in the browser.  Below is an image of what you should see.
    img.png

Need help, checkout the ServiceCardBottom component on this gist.

Add ActiveHelp Component.

  1. Add an ActiveHelp component to ServiceCardBottom.js file above the ServiceCardBottom class component.  
    const ActiveHelp = () =>
      <div className="active-help-dialog">
        <h4>Active Help <i className="fa fa-close" role="button" tabIndex="0" /></h4>
      </div>;
    
    Note: It is best practice to have a component declared prior to it being used.
  2. Add a handleOpen method to the ServiceCardBottom class.  (There are comment where code should be added.)  
      handleOpen() {
        this.setState({
          visible: true,
          // Set click event x position
          // Set click event y position
        });
      }
    
  3. Bind the this keyword to the handleOpen method in the constructor.  
    this.handleOpen = this.handleOpen.bind(this);
    
  4. Add an on click property to the question mark icon html element and assign the handleOpen method to it.  
    onClick={this.handleOpen}
    
  5. Add ActiveHelp component to the ServiceCardBottom class.  
        { this.state.visible ?
              <ActiveHelp
                // pass X position as a property
                // pass Y position as a property
                // pass handleClose method as a property
                // pass help as a property
                // eslint-disable-next-line
              />
              : null}
    

Note: The value of the visible property will determine if the ActiveHelp dialog will display.

Add Style to Active Help 

  1. Navigate to the scss directory: src/assets/styles/scss.
  2. Create a new file called _activeHelp.scss.
  3. Open _activeHelp.scss and add below css styles 
    /* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
     *   Active Help
     *~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */
    
    /* Help Dialog
     *-------------------------------------- */
    
    .active-help-dialog {
      background-color: $white;
      border: 1px solid $gray;
      position: fixed;
      right: 0;
      top: 15rem;
      width: 25rem;
      font-family: "Helvetica Neue",Helvetica,Arial,sans-serif;
      font-size: 1.4rem;
      text-transform: none;
      line-height: 2rem;
      padding: .5rem;
      z-index: 2000;
    }
    
    .active-help-dialog h4{
      margin-top: 0;
      display: inline-block;
    }
    
    .float-right {
        float: right;
    }
    
    /* Help Icon
     *-------------------------------------- */
    .active-help-icon {
      float: right;
    }
    
  4. Add _activeHelp.scss to the master.scss file.  Add below code to line 36  
    @import "activeHelp";
    
  5. Save all files and view result in the browser.
  6. Click on one of the questions marks.  A dialog that looks like the image below should appear.
    img2.PNG

Add close method and addition style.

Now ActiveHelp dialogs can be opened but not closed.  The X in the dialog box will close the dialog.  The handleClose method on the parent changes the visibility of the dialog. The child needs to change the visibility property on the parents state.  This means that the handleClose method will need to be passed down to the child.

  1. Add handleClose method to the ServiceCardBottom class component.  
      handleClose() {
        this.setState({
          visible: false,
        });
      }
    
  2. bind the this keyword to the handleClose method in the ServiceCardBottom constructor.
  3. Add handleClose property to ActiveHelp component and assign the handleClose method to the property inside the ServiceCardBottom component.
    •   Replace // pass handleClose method as a property comment.
      handleClose={this.handleClose}
      
  4. Destructure handleClose off of the props in the ActiveHelp component.
  5. Add an on click event to the close icon html element and assign handleClose to the event. 
    onClick={handleClose}
    
  6. Save the files and view in the browser.  Now the dialog can be opened and closed.  Give it a try.

Pass X and Y coordinates to the ActiveHelp component

  1. Add positionX and positionY to the handleOpen method's setState call. 
    positionX: event.clientX,
    positionY: event.clientY,
    
  2. Add event as a parameter to the handleOpen method
  3. Destructure the ServiceCardBottom state.
    • This will be in the render method.  
    • There is a comment to help. 
      const { visible, positionY, positionX } = this.state;
      
  4. Pass positionX and positionY to the ActiveHelp component. 
    positionX={positionX}
    positionY={positionY}
    
  5. Remove this.state from the visible property to fix the eslint error.
  6. Destructure positionX and positionY from the props in the ActiveHelp component.
  7. Place block statement around arrow function.
    Note: This means to wrap the body of the function in curly braces.
  8. The JSX will need to be returned now that the arrow function body is wrapped in curly braces. Add return () around the JSX.
  9. Assign posX and posY as offsets about the return statement inside of ActiveHelp component.
    const posX = positionX - 260;
    const posY = positionY - 15;
    
  10. Add inline style to outer div html element of the ActiveHelp component. 
    style={{ left: posX, top: posY }}
    
  11. Replace <h4>Active Help <i className="fa fa-close" role="button" tabIndex="0" /></h4> with below code. 
    <div>
      <h4>Active Help</h4>
      <h4 className="float-right"><i className="fa fa-close" role="button" tabIndex="0" onClick={handleClose} /></h4>
    </div>
    
  12. Open _activeHelp.scss and remove top and right css properties from the active-help-dialog class.
  13. Save all files and view in the browser.  The dialog should appear next to the question mark icon like the image below.
    img3.PNG

Show custom text in the dialog

  1. Open management console and create a form attribute definition called Help.
  2. Add the Help attribute to the "Cleaning" form and give it a text value.
  3. Navigate to the constants.js file: src/constancs.js
  4. On line 20 add the below code 
    export const ATTRIBUTE_HELP = 'Help';
    
  5. Navigate to the models directory and open index.js: src/models/index.js
  6. Add below code on line 10. 
    help: getAttributeValue(object, constants.ATTRIBUTE_HELP),
    
  7. Goto the ServiceCardBottom class component and pass help to the ActiveHelp component. 
    help={form.help}
    
  8. Destructure help from the props in the ActiveHelp component.
  9. Add the help to the ActiveHelp component between the closing div html tags. 
    <p className="ellipsis">{help}</p>
    
  10. Add a conditional around the question mark icon html element. 
    { form.help ?
      <i className="fa fa-question-circle 2x active-help-icon" title="Help" role="button" tabIndex="0" onClick={this.handleOpen} />
      : null }
    
  11. Save the files and view in the browser.
    • Only the "Cleaning" form should have a question mark icon next to it.

To see the completed ServiceCardBottom component visit the github repo.