DEV Community

Sami Ekblad
Sami Ekblad

Posted on • Edited on

Creating Custom Component for NPS Feedback

(Update 2023-05-24: The component is available as add-on at vaadin.com/directory/component/nps.)

We had a project that needed to implement the famous Net Promoter Score or NPS feedback. Quite boring, TBH. Just one of those relatively trivial UIs that you need to have. ("Does this spark joy?" It would most definitely be a clear "No." ).

Image description

However, there is a good reason for collecting user feedback like this, and we wanted to have it. I immediately thought that something like this would make sense as a separate component (mainly to avoid doing it again), so I decided to do that.

Here is the process I went through to build a reusable version. This blog is again for Vaadin, but the principles and the "build reusable components" -thinking makes sense on any web or IU framework.

Create a new add-on project

The first step in reusability: We want to create a separate add-on project. I used the Vaadin default addon-template from GitHub and created a new repository under my account. This project is a good starting point as it provides the boilerplate for packaging and distribution.

Clone the project locally

Next, clone the repository project locally in Visual Studio Code. I'm using the Dev Containers to run the project separately from others. There is a ready-made Java container to which I add Maven as a build system.

Vaadin Tip: Upgrade to V24
At the time of writing, the template project was still on older Vaadin 23. So a minor update is needed to pom.xml to make it compatible with the latest Vaadin version.

  • Vaadin version to 24.0.5
  • Java version to 17
  • Jetty version to 11.0.15

The UI Code

Like I said in the beginning, the UI is pretty simple, and the code to create that it's not too much:

public NPS() {
    Span title = new Span("On a scale of 1 to 10, how likely would you recommend this to your friend or colleague?");
    HorizontalLayout buttons = new HorizontalLayout();
    buttons.add(new Text("Not likely"));
    for (int i = 1; i <= 10; i++) {
        Button button = new Button(String.valueOf(i), e -> {
            int score = Integer.parseInt(e.getSource().getText());
            Notification.show("Changed to "+score);
        });  
        buttons.add(button);
    }
    buttons.add(new Text("Very likely"));
    this.add(title, buttons);    
}
Enter fullscreen mode Exit fullscreen mode

So the title and list of buttons in a horizontal layout. This is already usable as-is in your Vaadin application if you like to try it and use "copy-paste reusability".

Thinking about component reusability

When making reusable components, you want to consider a few things. The simplest way to document your component is to create a self-explanatory API.

  • Component UX. Do you need keyboard navigation? Mouse hover? Shortcuts? I suggest staying safe, combining existing components, and making your custom solutions sparingly.
  • Component configuration or DX. You want to be able to change the text and behaviour in various ways. The combinations can get tricky, so you want to have various scenarios well-tested.
  • UI integration. Is your component to be used alone (i.e., single-component app)? Will it be in a pop-up, or will it be part of that other UI? Data integration. Are your component emitting events? Does it support data binding? How much data to cache on the server, and how much can the browser handle?
  • Compatibility. What framework version are you using? What version do you expect your user to be using? How about the Java version? Remember, dependencies work much like Venn diagrams, so keep the list short.
  • Testing. How to automate tests. As said, the configuration of states might get tricky, so you want to be good here to make sure to find the edge cases.
  • Documentation. This part should be straightforward if you did good job with the API. Focus on describing the developer use cases and skip the "this method does X" style docs altogether.

It might take a round or two to get everything right, but best to start with a "real app" where you use it and write code that uses your component. Not aiming for full-blown TDD, this is what I made for testing:

final NPS nps = new NPS();
nps.setId("nps");  // This is for automated tests

// Button to start the feed process
add(new Button("Ask NPS", e -> {
    add(nps);
}));

// Get the score and remove component
nps.addValueChangeListener(e-> {
    Notification.show("Value changed from "+e.getOldValue()+" to "+e.getValue());
    add(new Paragraph("NPS: " +e.getOldValue()+" -> "+e.getValue()));
    remove(nps);
});

// Edit the the score
add(new Button("Edit score", e -> {
    nps.setValue((int)Math.ceil(Math.random()*10));
    add(nps);
}));
Enter fullscreen mode Exit fullscreen mode

This is implemented in AddonView.java, also used for automated tests. It is not the real thing, but simulating scenarios we had in the actual application.

The basic business logic

Since the NPS component is clearly a "field component" with a single editable value, we can reuse the framework functionality here. Extending CustomField gives us most of the functions we need for value input and events. We only need to implement the following:

protected Integer generateModelValue() {
    return this.score;
}

protected void setPresentationValue(Integer score) {
    this.score = score;        
}
Enter fullscreen mode Exit fullscreen mode

Our test case already revealed a problem with the read-only support. We want the value to be visible, yet the input is disabled. The requires two more methods to be implemented.

First, highlighting the selection. We use the "success color" to do that.

private void updateButtonSate() {
    this.buttons
        .getChildren()
        .filter(c -> c instanceof Button)
        .forEach(c -> {
            Button b = (Button)c;
            if (score != null && score.equals(Integer.parseInt(b.getText()))) {
                b.addClassName(LumoUtility.Background.SUCCESS_50);
                b.addClassName(LumoUtility.TextColor.SUCCESS_CONTRAST);
            } else {
                b.removeClassName(LumoUtility.Background.SUCCESS_50);
                b.removeClassName(LumoUtility.TextColor.SUCCESS_CONTRAST);
            };
        });
}
Enter fullscreen mode Exit fullscreen mode

Then another one to disable the buttons when we are in read-only state.

public void setReadOnly(boolean readOnly) {
    super.setReadOnly(readOnly);
    this.buttons
        .getChildren()
        .filter(c -> c instanceof Button)
        .forEach(c -> ((Button)c).setEnabled(!readOnly));
}
Enter fullscreen mode Exit fullscreen mode

Now we have the events, we got the component value bookkeeping.

Release, release

"Release early, release often", correct? This is all you need to create a simple application that collects NPS feedback from your users. As you see, it is only the web UI part, and you still need to store data and calculate the final score.

You may have your custom database, and we still need to package the component for distribution, but I'll show some simple options in the next blog post. In the meantime: checkout the component at github.com/samie/nps.

This reusability actually sparks joy, but now it is your turn: On a scale of 1 to 10, how likely would you recommend this NPS survey to your friend or colleague? Leave a comment below.

Top comments (0)