Creating a Debugging Interface in Godot (Part 3)

Welcome to Part 3 of my tutorial for creating a debugging interface in Godot! In Part 1, we created the base for our debugging system, and in Part 2 we created debug widgets to show our debugging information. If you haven’t read those parts, you would be advised to do so before continuing on with this part. Alternatively, if you want to start from this part, and just need the end code from the preceding parts, you can check out the tutorial-part-2 branch from the Github repo.

At this point, we have a debugging interface that we can toggle on and off, and we have a base DebugWidget class to build our debug widget from, as well as a DebugTextList debug widget. We don’t quite have everything we’d ideally want in a debugging system, though. What happens if we want to display different debug widgets, and not have to see all of them at the same time? What if we have a lot of debug widgets, so much so that they take up most of the screen space, making it impossible to see the underlying game beneath the cluttered visuals?

We could try creating multiple DebugLayer nodes, but this would quickly become brittle and clunky. As the DebugLayer is exposed globally for our code to access, any additional DebugLayer nodes would also need to be global, which would pollute the AutoLoad declarations. It would also mean having to remember which DebugLayer you’re connecting to, as well as assigning different keys to show and hide each layer so that they don’t all show at the same time… Suffice it to say, doing things this way is awful.

It would be better to create a system specifically for showing different debugging interfaces, depending on whatever criteria we choose to specify. We’ll do this by creating a new type of node, the DebugContainer, and modifying DebugLayer to be capable of managing multiple DebugContainer nodes.

If you want to see the final result, you can check out the tutorial-part-3 branch in the Github repo.

Ready? Let’s go!

Creating the DebugContainer

Begin by creating a new script file, DebugContainer.gd, in the _debug directory. Have it extend MarginContainer. We’ll begin by adding this line of code:


# The list of widget keywords associated with the DebugContainer.
var _widget_keywords = {}

Wait a minute, you say. That looks suspiciously like the code we added to DebugLayer.gd in the previous part of this tutorial. Well, you’re right! That’s exactly what it is. Our goal is to move management of individual DebugWidget nodes out of DebugLayer and into DebugContainer nodes, so it makes sense to go ahead and store the widget keywords here.

Moving Widget Code from DebugLayer to DebugContainer

In fact, we’re going to move most of the code we added to DebugLayer for managing debug widgets into DebugContainer.gd. Let’s take care of that right now:


func _ready():
    mouse_filter = MOUSE_FILTER_IGNORE
    _register_debug_widgets(self)
    Debug.register_debug_container(self)


# Adds a widget keyword to the registry.
func _add_widget_keyword(widget_keyword: String, widget_node: Node) -> void:
  var widget_node_name = widget_node.name if 'name' in widget_node else str(widget_node)

  if not _widget_keywords.has(widget_node_name):
    _widget_keywords[widget_node_name] = {}

  if not _widget_keywords[widget_node_name].has(widget_keyword):
    _widget_keywords[widget_node_name][widget_keyword] = widget_node
  else:
    var widget = _widget_keywords[widget_node_name][widget_keyword]
    var widget_name = widget.name if 'name' in widget else str(widget)
    push_error('DebugContainer._add_widget_keyword(): Widget keyword "' + widget_node_name + '.' + widget_keyword + '" already exists (' + widget_name + ')')
    return


# Go through all children of provided node and register any DebugWidgets found.
func _register_debug_widgets(node) -> void:
  for child in node.get_children():
    if child is DebugWidget:
      register_debug_widget(child)
    elif child.get_child_count() > 0:
      _register_debug_widgets(child)


# Register a single DebugWidget to the DebugContainer.
func register_debug_widget(widgetNode) -> void:
  for widget_keyword in widgetNode.get_widget_keywords():
    _add_widget_keyword(widget_keyword, widgetNode)


# Sends data to the widget with widget_name, triggering the callback for widget_keyword.
func update_widget(widget_path: String, data) -> void:
  var split_widget_path = widget_path.split('.')
  if split_widget_path.size() == 1 or split_widget_path.size() > 2:
    push_error('DebugContainer.update_widget(): widget_path formatted incorrectly. ("' + widget_path + '")')

  var widget_name = split_widget_path[0]
  var widget_keyword = split_widget_path[1]

  if _widget_keywords.has(widget_name) and _widget_keywords[widget_name].has(widget_keyword):
    _widget_keywords[widget_name][widget_keyword].handle_callback(widget_keyword, data)
  else:
    push_error('DebugContainer.update_widget(): Widget name and keyword "' + widget_name + '.' + widget_keyword  + '" not found (' + str(_widget_keywords) + ')')

Almost all of the code above is code we worked on in Part 2 of this tutorial. If you need any refreshers on how that code works, feel free to review that part.

There are a couple of differences to the code that need to be pointed out; both are in the _ready() function. First, the mouse_filter = MOUSE_FILTER_IGNORE line.

By default, mouse_filter is equal to MOUSE_FILTER_PASS. That value means that, when you render a UI node, mouse interactions are captured by the first UI element that decides to handle it. If you have two UI nodes, and you click on that stack, the “top” node will receive the mouse event first. If it doesn’t handle the event, it gets passed to any nodes below it. If it does do something with the event, however, then the event is considered to be handled, and is no longer passed on to other nodes.

With that information, let’s think about how our debugging system is implemented. We made DebugLayer a CanvasLayer node that is rendered at the highest level possible. Because of this, anything in DebugLayer will receive mouse events before anything else in the game. Since control nodes default to using the MOUSE_FILTER_PASS setting, that means DebugLayer will consume any mouse events while it is being shown, preventing interaction with the underlying game. That is behavior we definitely don’t want. That is why we set mouse_filter to MOUSE_FILTER_IGNORE for DebugContainer, so that it will ignore any mouse events, allowing them to proceed down to the underlying game nodes.

The other thing to note about the code we’re adding is the call to Debug.register_debug_container(). This will be how our debug container registers itself with the DebugLayer, much like what we did with debug widgets in the previous part of the tutorial.

If you’re copying code over from your project, don’t forget to update the error messaging and code documentation to say DebugContainer instead of DebugLayer.

Modifying DebugLayer to use DebugContainers

We’re going to need to add register_debug_container() to DebugLayer.gd. Before we do so, however, we need to make some other changes to the DebugLayer scene, itself:

  • Remove the TextList1 node we created in the previous tutorial; we’re no longer going to store debug widgets directly in the DebugLayer scene.
  • Select the DebugUIContainer node, click on the Layout tab, and select “Full Screen”.
  • Add a VBoxContainer child to DebugUIContainer.
  • Add a Tabs node and a MarginContainer node as children of the VBoxContainer (in that order).
  • Name those last two nodes DebugTabs and DebugContentContainer.
  • Go to the DebugTabs node properties and set Tab Alignment to left.

That takes care of the scene. Let’s move on to modifying the script. If you haven’t done so already, remove the code implementing debug widgets in DebugLayer (aka the stuff we moved into DebugContainer). Once that’s done, add the register_debug_container() function and the related code that is part of its implementation:


signal debug_container_registered


# The debug containers registered to the DebugLayer.
var _debug_containers = {}

# The currently active debug container.
var _debugContainer: Node

# Nodes implementing the debug container tab switching interface.
onready var debugTabs = $DebugUIContainer/VBoxContainer/DebugTabs
onready var debugContentContainer = $DebugUIContainer/VBoxContainer/DebugContentContainer


func _input(_event) -> void:
  if Input.is_action_just_pressed('toggle_debug_interface'):
    # ...existing code 
    _debugContainer.show()


func register_debug_container(containerNode) -> void:
  var container_name = containerNode.name
  if _debug_containers.has(container_name):
    push_error('DebugLayer.register_debug_container: Debug already has registered DebugContainer with name "' + container_name + '".')
    return

  # Reparent the container node to the DebugLayer.
  containerNode.get_parent().call_deferred('remove_child', containerNode)
  debugContentContainer.call_deferred('add_child', containerNode)
  debugTabs.add_tab(container_name)

  _debug_containers[container_name] = containerNode
  if _debug_containers.size() == 1:
    _debugContainer = containerNode

  # Hide this container node so we don't show debug info by default.
  containerNode.hide()

  emit_signal('debug_container_registered', containerNode)

That’s quite a chunk of code. Let’s unpack this and see what everything does.

First, we add a signal, debug_container_registered, which we’ll dispatch whenever a debug container is registered. Next, we add _debug_containers, which will be used the same way that we used _debug_widgets, just for debug containers instead of debug widgets. We also add _debugContainer to keep track of the currently shown debug container’s node.

We define references for two of the UI nodes we added to the DebugLayer scene, debugTabs and debugContentContainer. For now, we’ll ignore these in favor of explaining other parts of the added code. Don’t worry, we’ll explain what these nodes are used for as we progress through the tutorial.

Continuing on, we modify our _input() function to show the current debug container node whenever we toggle on the debug interface. And finally, at long last, we have the register_debug_container() function, itself.

In register_debug_container(), we first get the name of the passed-in containerNode and check to see if that name is already registered; if it is, we show an error and return without doing anything else. Next, we need to reparent the containerNode from wherever it currently is in the scene tree to become a child of debugContentContainer. Note the use of call_deferred(), rather than invoking the functions directly; this calls the specified functions during Godot’s idle time, which prevents issues that can occur when running code within nodes that are being reparented.

We’re going to allow DebugContainer nodes to be added pretty much wherever we want when creating our scenes, so we need to move them inside the DebugLayer at runtime to ensure they get displayed as part of the debugging interface. This should make more sense once we get to the part where we start using debug containers.

After the reparenting is finished, we add a new tab to the DebugTabs node, entitled the debug container’s name. Then we add the containerNode to the dictionary of debug containers; if it’s the first debug container we’ve registered, we set it to be the initially-shown debug container. We want to make sure that our debug containers aren’t visible by default (otherwise, we’ll see every debug container all at once), so we call hide() on the containerNode. Finally, we emit the debug_container_registered signal, so anything that wants to listen for that will know when a debug container is registered, and which one it is.

I have not needed to make use of this signal yet in my personal use of the debugging system, but it seems like a potentially useful thing to expose, so it makes sense to go ahead and do so.

Now that we’ve implemented the register_debug_container() function, it’s time to take a closer look at the DebugTabs node and make it work.

DebugTabs

The Tabs node in Godot is a basic tabs implementation. It does no view switching by itself; instead, when we switch between tabs, a signal is fired indicating which tab was switched to, and it’s up to our code to listen for that signal and respond to it. We’re going to use this functionality to change which debug container is the one being shown in DebugLayer.

Godot does provide a TabsContainer node, which would implement both the tabs and view switching. However, since it is a single node, if you ignore mouse events (as we mentioned needing to add for DebugContainer), then you can’t click on the tabs. If you leave the node able to capture mouse events, it will prevent interacting with the game when the debug interface is open. Thus, I’ve opted to just use the Tabs node and implement view switching manually.

The code to implement the view switching is rather simple:


func _ready() -> void:
  # ...existing code
  debugTabs.connect('tab_changed', self, '_on_Tab_changed')


func _on_Tab_changed(tab_index) -> void:
  var tab_name = debugTabs.get_tab_title(tab_index)
  var containerNode = _debug_containers[tab_name]
  _debugContainer.hide()
  _debugContainer = containerNode
  _debugContainer.show()

During _ready(), we connect to the tab_changed signal for debugTabs and provide an _on_Tab_changed() callback. In the callback, we get the name of the tab (based on the tab_index provided as the callback function’s argument), and use that name to find the registered debug container with matching name. We then hide the currently-visible debug container, switch the _debugContainer variable to be the upcoming containerNode, and then make that debug container visible.

Updating Widgets

We’re still missing one important functionality: sending data to update our debug widgets. Since we moved our previous implementation of update_widget() into the DebugContainer node, we’ll need to create a new version of update_widget() that determines which debug container the widget data should be sent to.


# Sends data to the debug container specified in widget_path.
# API: container_name:widget_name.widget_keyword
func update_widget(widget_path: String, data = null) -> void:
  var split_keyword = widget_path.split(':')
  if split_keyword.size() == 1:
    push_error('DebugLayer.update_widget(): No container name was specified. (' + widget_path + ', ' + str(data) + ')')
    return

  var container_name = split_keyword[0]
  if not _debug_containers.has(container_name):
    push_error('DebugLayer.update_widget(): Container with name "' + container_name + '" is not registered.')
    return

  var containerNode = _debug_containers[container_name]
  widget_path = split_keyword[1]
  containerNode.update_widget(widget_path, data)

Notice that the arguments are still the same: we’re passing in a widget_path and data. However, we need a way to indicate which debug container has the debug widget we want to update.

To do this, we’re going to modify the widget_path API slightly. Instead of starting the string with the name of the debug widget, we’ll start with the name of the debug container, and delimit it with a colon, :.

We implement this in code by splitting the widget_path string on said colon and verifying that there was indeed a debug container name passed in. If no container name was provided, then we show an error and return without doing anything further; we do the same if the provided debug container’s name doesn’t exist in our dictionary of registered debug containers. If all is valid, then we get the specified debug container and call its update_widget() function, passing in the other half of our split string (aka the original widget_name.widget_keyword API), as well as data.

At this point, we’re almost ready to run the test scene to try our changes, but there’s something we need to do first: modify our test scene to support the changes we’ve made to our Debug API.

Adding a DebugContainer to the Test Scene

Let’s go straight to our TestScene scene and add one of our new DebugContainer nodes; name it “TestDebugContainer”. As a child of that, add a DebugTextList debug widget with the name “TextList1”. Finally, go to TestScene.gd and change our call to Debug.update_widget() to incorporate our new syntax for specifying the debug container in the widget_path.


func _process(_delta) -> void:
  # ...existing code
  elif test_ct == 900:
    Debug.update_widget('TestDebugContainer:TextList1.remove_label', { 'name': 'counter' })
  elif test_ct < 900:
    Debug.update_widget('TestDebugContainer:TextList1.add_label', { 'name': 'counter', 'value': str(test_ct) })

Now we can run the test scene and see our changes in action! If you press the debug toggle key combination we defined earlier (Shift + `), you should be able to see the same counting text that we saw before. Additionally, you should be able to see the tab we just added, titled "TestDebugContainer".

If that's what you see, good job! If not, review the tutorial (and perhaps the repo code) to try and identify where things went wrong.

Testing with Multiple Debug Containers

That said, these are things we've seen before (aside from the tab). We made these changes to support being able to show different debugging views via multiple debug containers. Let's go ahead and add another one!

Duplicate the TestDebugContainer node (which will create a copy of both that node and the child debug widget; the TestDebugContainer node will be automatically named "TestDebugContainer2"), then go to TestScene.gd and add two new calls to Debug.update_widget() as shown below:


# ...existing code
  elif test_ct == 900:
    Debug.update_widget('TestDebugContainer:TextList1.remove_label', { 'name': 'counter' })
    Debug.update_widget('TestDebugContainer2:TextList1.remove_label', { 'name': 'counter' })
  elif test_ct < 900:
    Debug.update_widget('TestDebugContainer:TextList1.add_label', { 'name': 'counter', 'value': str(test_ct) })
    Debug.update_widget('TestDebugContainer2:TextList1.add_label', { 'name': 'counter', 'value': str(round(test_ct / 10)) })

As you can see, we're simply changing the widget_path to request TestDebugContainer2 instead of TestDebugContainer. To keep the test simple, our second call is showing the same test_ct variable, but divided by ten and rounded to the nearest integer.

That's it! No, seriously! Go ahead and run the scene again, and everything should "just work". You'll see two tabs, one named "TestDebugContainer" and the other named "TestDebugContainer2". Switching between them will alternate between showing the original counter and the rounded version.

But wait, there's more! We can add these debug containers anywhere in our scene structure, and as long as those scenes are part of the currently-running scene tree they'll register themselves to our debugging interface.

To test this, let's create a quick child scene to add to our test scene. Create a new scene called "TestChild" (based on Node), then add a button with text "Test Button" and place it near the top-center of the child scene. Add a DebugContainer with DebugTextList child to TestChild, and make sure you rename them to "TestDebugContainer2" and "TextList1" (to match the widget_path we've defined in the TestScene.gd script). Instance TestChild into TestScene and remove the TestDebugContainer2 node that was in TestScene.

Run the test scene, and you get exactly the same result as before. You can see both the tabs, and switch between them with ease. The only difference is that one debug container originated in TestScene, and the other in TestChild.

If you see the TestDebugContainer2 tab, but not the counter, that means you forgot to make the debug node names and the widget_key string match, so you're not actually sending updates to the correct location.

Fixing One Last Bug

Before we get too hyped with excitement, however, there is a bug that we need to take care of. Run the test scene, open the debugging interface, and hover over the button we added to the TestChild scene. Nothing seems to happen, right? Now close the debugging interface and hover over the button again. This time, it lights up, indicating that it's receiving mouse events. That means something in our debugging interface is intercepting mouse events.

Fortunately, this is a simple fix: we just need to go to the DebugLayer scene and change the mouse_filter properties for DebugUIContainer, VBoxContainer, and DebugContentContainer to MOUSE_FILTER_IGNORE (shown as just Ignore in the editor interface). Do not, however, change the mouse_filter property for DebugTabs, or you may find yourself unable to click on the tabs at all!

Once you've made those changes, run the test scene again. This time, you should be able to trigger the button hover state when the debug interface is open.

Congratulations!

We now have DebugContainer nodes, which we can add wherever we want, and add whatever debug widgets we want to them, using the tabbed interface to switch between whichever debugging views we want to see. And best of all, it's simple to add these debug containers and widget as we need them, whether for temporarily reporting data or for permanent display of debugging information.

With these things, you have the essentials needed to build on this debugging system and make it your own. Create widgets that show you the information that you need to know. As shown in this tutorial, it's easy to make a new debug widget, and just as easy to register it to the debugging system. Using this system has definitely made my game development much easier, and I hope the same will be true for you!

If you want to see the code as it was at the end of this part, check out the tutorial-part-3 branch in the Github repo.

Creating a Debugging Interface in Godot (Part 2)

Welcome to Part 2 of my tutorial for creating a debugging interface in Godot! In Part 1, we created the base for our debugging system. If you haven’t read that part, you should do so now, because the rest of the tutorial series will be building atop it. Alternatively, if you just want the code from the end of Part 1, you can check out the tutorial-part-1 branch in the Github repo.

At this point, we have the base of a debugging system, but that’s all it is: a base. We need to add things to it that will render the debugging information we want to show, as well as an API to DebugLayer that is responsible for communicating this information.

We’ll do this through “debug widgets”. What’s a debug widget? It’s a self-contained node that accepts a set of data, then displays it in a way specific to that individual widget. We’ll make a base DebugWidget node, to provide common functionalities, then make other debug widgets extend that base that implement their custom functionalities on top of the base node.

Alright, enough high-level architecture talk. Let’s dive in and make these changes!

Creating the Base DebugWidget

To get started, we want a place to store our debug widgets. To that end, make a new directory in _debug, called widgets. In this new widgets directory, create a new script called DebugWidget.gd, extending MarginContainer.


# Base class for nodes that are meant to be used with the DebugLayer system.
class_name DebugWidget
extends MarginContainer

Note the custom class_name. This is important, because later on we’ll be using it to check whether a given node is a debug widget.

You may need to reload your Godot project to ensure that the custom class_name gets registered.

Next, we’re going to add something called “widget keywords”:


# Abstract method which must be overridden by the inheriting debug widget.
# Returns the list of widget keywords. Responses to multiple keywords should be provided in _callback.
func get_widget_keywords() -> Array:
  push_error("DebugWidget.get_widget_keywords(): No widget keywords have been defined. Did you override the base DebugWidget.get_widget_keywords() method?")
  return []

This function will be responsible for returning a debug widget’s widget keywords. What are widget keywords, though?

To give a brief explanation, widget keywords are the way we’re going to expose what functionalities this debug widget provides to the debugging system. When we want to send data to a widget, the debugging system will search through a list of stored widget keywords, and if it finds one matching the one we supply in the data-sending function, it will run a callback associated with that widget keyword.

If that doesn’t make much sense right now, don’t worry. As you implement the rest of the flow, it should become clearer what widget keywords do.

One thing to note about the code is that we’re requiring inheriting classes to override the method. This is essentially an implementation of the interface pattern (since GDScript doesn’t provide an official way to do interfaces).

Let’s add a couple more functions to DebugWidget.gd:


# Abstract method which must be overridden by the inheriting debug widget.
# Handles the widget's response when one of its keywords has been invoked.
func _callback(widget_keyword, data) -> void:
  push_error('DebugWidget._callback(): No callback has been defined. (' + widget_keyword + ', ' + data + ')')


# Called by DebugContainer when one of its widget keywords has been invoked.
func handle_callback(widget_keyword: String, data) -> void:
  _callback(widget_keyword, data)

handle_callback() is responsible for calling the _callback() function. Right now, that’s all it does. We’ll eventually also do some pre-callback validation in this function, but we won’t get into that just yet.

_callback() is another method that we explicitly want the inheriting class to extend. Essentially, this is what will be run whenever something uses one of the debug widget’s keywords. Nothing is happening there right now; all the action is going to be in the inheriting debug widgets.

That’s it for the base DebugWidget. Time to extend that base!

Creating the DebugTextList DebugWidget

Remember that DebugLabel that was discussed at the beginning of the article? Having a text label that you can update as needed is a useful thing for a debugging system to have. Why stop with a single label, though? Why not create a debug widget that is a list of labels, which you can update with multiple bits of data?

That’s the debug widget we’re going to create. I call it the DebugTextList.

I prefix debug widget nodes with Debug, to indicate that they are only meant to be used for debugging purposes. It also makes it easy to find them when searching for scenes to instance.

Create a directory in widgets called TextList, then create a DebugTextList scene (not script). If you’ve registered the DebugWidget class, you can extend the scene from that; otherwise, this is the point where you’ll need to reload the project in order to get access to that custom class.

Why create it as a scene, and not as another custom node? Really, it’s simply so that we can create the node tree for our debug widget using the editor’s graphical interface, making it simpler to understand. It’s possible to add the same child nodes through a script, and thereby make it possible to make the DebugTextList a custom node. For this tutorial, however, I’m going to keep using the scene-based way, for simplicity.

Alright, let’s get back on with the tutorial.

Add a VBoxContainer child node to the DebugTextList root node. Afterwards, attach a new script to the DebugTextList scene, naming it DebugTextList.gd, and have it extend DebugWidget. Replace the default script text with the following code:


const WIDGET_KEYWORDS = {
  'ADD_LABEL': 'add_label',
  'REMOVE_LABEL': 'remove_label'
}


onready var listNode = $VBoxContainer

listNode is a reference to the VBoxContainer. We also have defined a const, WIDGET_KEYWORDS, which will define the widget keywords this debug widget supports. Technically, you could just use the keyword’s strings where needed, rather than define a const, but using the const is easier, as you can see below.


# Handles the widget's response when one of its keywords has been invoked.
func _callback(widget_keyword: String, data) -> void:
  match widget_keyword:
    WIDGET_KEYWORDS.ADD_LABEL:
      add_label(data.name, str(data.value))
    WIDGET_KEYWORDS.REMOVE_LABEL:
      remove_label(data.name)
    _:
      push_error('DebugTextList._callback(): widget_keyword not found. (' + widget_keyword + '", "' + name + '", "' + str(WIDGET_KEYWORDS) + '")')


# Returns the list of widget keywords.
func get_widget_keywords() -> Array:
  return [
    WIDGET_KEYWORDS.ADD_LABEL,
    WIDGET_KEYWORDS.REMOVE_LABEL
  ]

Notice that we’re overriding both _callback() and get_widget_keywords(). The latter returns the two widget keywords we defined in the const, while the former performs a match check against the widget_keyword argument to see if it matches one of our two defined keywords, running a corresponding function if so. By using the const to define our widget keywords, we’ve made it easier to ensure that the same values get used in all the places needed in our code.

match is Godot’s version of implementing the switch/case pattern used in other languages (well, it’s slightly different, but most of the time you can treat it as such). You can read more about it here. The underscore in the match declaration represents the default case, or what happens if widget_keyword doesn’t match our widget keywords.

Let’s go ahead and add the two response functions now: add_label() and remove_label(). We’ll also add a helper function that is used by both, _find_child_by_name().


# Returns a child node named child_name, or null if no child by that name is found.
func _find_child_by_name(child_name: String) -> Node:
  for child in listNode.get_children():
    if 'name' in child and child.name == child_name:
      return child

  return null


# Adds a label to the list, or updates label text if label_name matches an existing label's name.
func add_label(label_name: String, text_content: String) -> void:
  var existingLabel = _find_child_by_name(label_name)
  if existingLabel:
    existingLabel.text = text_content
    return

  var labelNode = Label.new()
  labelNode.name = label_name
  labelNode.text = text_content
  listNode.add_child(labelNode)


func remove_label(label_name) -> void:
  var labelNode = _find_child_by_name(label_name)
  if labelNode:
    listNode.remove_child(labelNode)

_find_child_by_name() takes a given child_name, loops through the children of listNode to see if any share that name, and returns that child if so. Otherwise, it returns null.

add_label() uses that function to see if a label with that name already exists. If the label exists, then it is updated with text_content. If it doesn’t exist, then a new label is created, given the name label_name and text text_content, and added as a child of listNode.

remove_label() looks for an existing child label, and removes it if found.

With this code, we now have a brand-new debug widget to use for our debugging purposes. It’s not quite ready for use to use, yet. We’re going to have to make changes to DebugLayer in order to make use of these debug widgets.

Modifying DebugLayer

Back in Part 1 of this tutorial, we made the DebugLayer scene a global AutoLoad, to make it accessible from any part of our code. Now, we need to add an API to allow game code to send information through DebugLayer to the debug widgets it contains.

Let’s start by adding a dictionary for keywords that DebugLayer will be responsible for keeping track of.


# The list of widget keywords associated with the DebugLayer.
var _widget_keywords = {}

Next, we’ll add in the ability to “register” debug widgets to the DebugLayer.


func _ready():
  # ...existing code
  _register_debug_widgets(self)


# Go through all children of provided node and register any DebugWidgets found.
func _register_debug_widgets(node) -> void:
  for child in node.get_children():
    if child is DebugWidget:
      register_debug_widget(child)
    elif child.get_child_count() > 0:
      _register_debug_widgets(child)


# Register a single DebugWidget to the DebugLayer.
func register_debug_widget(widgetNode) -> void:
  for widget_keyword in widgetNode.get_widget_keywords():
    _add_widget_keyword(widget_keyword, widgetNode)

In our _ready() function, we’ll call _register_debug_widgets() on the DebugLayer root node. _register_debug_widgets() loops through the children of the passed-in node (which, during the ready function execution, is DebugLayer). If any children with the DebugWidget class are found, it’ll call register_debug_widget() to register it. Otherwise, if that child has children, then _register_debug_widgets() is called on that child, so that ultimately all the nodes in DebugLayer will be processed to ensure all debug widgets are found.

register_debug_widget(), meanwhile, is responsible for looping through the debug widget’s keywords (acquired from calling get_widget_keywords()) and adding them to the keywords dictionary via _add_widget_keyword(). Note that this function I chose to not mark as “private” (by leaving off the underscore prefix). There may be reason to allow external code to register a debug widget manually. Though I personally haven’t encountered this scenario yet, the possibility seems plausible enough that I decided to not indicate the function as private.

Let’s add the _add_widget_keyword() function now:


# Adds a widget keyword to the registry.
func _add_widget_keyword(widget_keyword: String, widget_node: Node) -> void:
  var widget_node_name = widget_node.name if 'name' in widget_node else str(widget_node)

  if not _widget_keywords.has(widget_node_name):
    _widget_keywords[widget_node_name] = {}

  if not _widget_keywords[widget_node_name].has(widget_keyword):
    _widget_keywords[widget_node_name][widget_keyword] = widget_node
  else:
    var widget = _widget_keywords[widget_node_name][widget_keyword]
    var widget_name = widget.name if 'name' in widget else str(widget)
    push_error('DebugLayer._add_widget_keyword(): Widget keyword "' + widget_node_name + '.' + widget_keyword + '" already exists (' + widget_name + ')')
    return

That looks like a lot of code, but if you examine it closely, you’ll see that most of that code is just validating that the widget data we’re working with was set up correctly. First, we get the name of widget_node (aka the name as entered in the Godot editor). If that node’s name isn’t already a key in our _widget_keywords dictionary, we add it. Next, we check to see if the widget_keyword already exists in the dictionary. If it doesn’t, then we add it, setting the value equal to the widget node. If it does exist, we push an error to Godot’s error console (after some string construction to make a developer-friendly message).

Interacting with Debug Widgets

At this point, we can register debug widgets so that our debugging system is aware of them, but we still don’t have a means of communicating with the debug widgets. Let’s take care of that now.


# Sends data to the widget with widget_name, triggering the callback for widget_keyword.
func update_widget(widget_path: String, data) -> void:
  var split_widget_path = widget_path.split('.')
  if split_widget_path.size() == 1 or split_widget_path.size() > 2:
    push_error('DebugContainer.update_widget(): widget_path formatted incorrectly. ("' + widget_path + '")')

  var widget_name = split_widget_path[0]
  var widget_keyword = split_widget_path[1]

  if _widget_keywords.has(widget_name) and _widget_keywords[widget_name].has(widget_keyword):
    _widget_keywords[widget_name][widget_keyword].handle_callback(widget_keyword, data)
  else:
    push_error('DebugContainer.update_widget(): Widget name and keyword "' + widget_name + '.' + widget_keyword  + '" not found (' + str(_widget_keywords) + ')')

Our API to interact with debug widgets will work like this: we’ll pass in a widget_path string to update_widget(), split with a . delimiter. The first half of the widget_path string is the name of the widget we want to send data to; the second half is the widget keyword we want to invoke (and thereby tell the widget what code to run).

update_widget() performs string magic on our widget_path, makes sure that we sent in a properly-formatted string and that the widget and widget keyword is part of _widget_keywords. If things were sent correctly, the widget node reference we stored during registration is accessed, and the handle_callback() method called, passing in whatever data the widget node expects. If something’s not done correctly, we alert the developer with error messages and return without invoking anything.

That’s all we need to talk to debug widgets. Let’s make a test to verify that everything works!

Currently, our TestScene scene doesn’t have an attached script. Go ahead and attach one now (calling it TestScene.gd) and add the following code to it:


extends Node

var test_ct = -1

func _process(_delta) -> void:
  test_ct += 1
  if test_ct >= 1000:
    test_ct = -1
  elif test_ct >= 900:
    Debug.update_widget('TextList1.remove_label', { 'name': 'counter' })
  else:
    Debug.update_widget('TextList1.add_label', { 'name': 'counter', 'value': str(test_ct % 1000) })

This is just a simple counter functionality, where test_ct is incremented by 1 each process step. Between 0-899, Debug.update_widget() will be called, targeting a debug widget named “TextList1” and the add_widget widget keyword. For the data we’re passing the widget, we send the name of the label we want to update, and the value to update to (which is a string version of test_ct). Once test_ct hits 900, however, we want to remove the label from the debug widget, which we accomplish through another Debug.update_widget() call to TextList1, but this time using the remove_label widget keyword. Finally, once test_ct hits 1000, we reset it to 0 so it can begin counting up anew.

If you run the test scene right now, though, nothing happens. Why? We haven’t added TextList1 yet! To do that, go to the DebugLayer scene, remove the existing test label (that we created during Part 1), and instance a DebugTextList child, naming it TextList1. Now, if you run the test scene and open up the debugging interface (with Shift + `, which we set up in the previous part), you should be able to see our debug widget, faithfully reporting the value of test_ct each process step.

If that’s what you see, congratulations! If not, review the tutorial code samples and try to figure out what might’ve been missed.

One More Thing

There’s an issue that we’re not going to run into as part of this tutorial series, but that I’ve encountered during my own personal use of this debugging system. To save future pain and misery, we’re going to take care of that now.

Currently, our code for debug widgets always assumes that we’re going to pass in some form of data for it to process. But what if we want a debug widget that doesn’t need additional data? As things stand, because debug widgets assume that there will be data, the code will crash if you don’t pass in any data.

To fix that, we’ll need to add a couple of things to the base DebugWidget class:


# Controls if the widget should allow invocation without data.
export(bool) var allow_null_data = false


# Called by DebugContainer when one of its widget keywords has been invoked.
func handle_callback(widget_keyword: String, data = null) -> void:
  if data == null and not allow_null_data:
    push_error('DebugWidget.handle_callback(): data is null. (' + widget_keyword + ')')
    return
  
  _callback(widget_keyword, data)

We’ve added an exported property, allow_null_data, defaulting it to false. If a debug widget implementation wants to allow null data, it needs to set this value to true.

handle_callback() has also been modified. Before it runs _callback(), it first checks to see if data is null (which it will be if the second argument isn’t provided, because we changed the argument to default to null). If data is null, and we didn’t allow that, we push an error and return without running callback(). That prevents the game code crashing because of null data, and it also provides helpful information to the developer. If there is data, or the debug widget explicitly wants to allow null data, then we run _callback(), as normal.

That should take care of the null data issue. At this point, we’re golden!

Congratulations!

Our debugging system now supports adding debug widgets, and through extending the base DebugWidget class we can create whatever data displays we want. DebugTextList was the first one we added, and hopefully it should be easy to see how simple it is to add other debug widgets that show our debugging information in whatever ways we want. If we want to show more than one debug widget, no problem, just instance another debug widget!

Even though all this is pretty good, there are some flaws that might not be immediately apparent. For instance, what happens if we want to implement debug widgets that we don’t want to be shown at the same time, such as information about different entities in our game? Or what if we want to keep track of so much debugging information that we clutter the screen, making it that much harder to process what’s going on?

Wouldn’t it be nice if we could have multiple debug scenes that we could switch between at will when the debug interface is active? Maybe we’d call these scenes “containers”. Or, even better, a DebugContainer.

That’s what we’ll be building in the next part of this tutorial!

If you want to see the complete results from this part of the tutorial, check the tutorial-part-2 branch of the Github repo.

Creating a Debugging Interface in Godot (Part 1)

At some point during the development of a game, you need to be able to show information that helps you debug issues in your game. What kind of information? That really depends on your game and what your needs are. It could be as simple as printing some text that shows the result of an internal calculation, or it could be as fancy as a chart showing the ratio of decisions being made by the game’s artificial intelligence.

There are different kinds of debugging needed as well. Sometimes, you need something temporary to help you figure out why that function you just wrote isn’t behaving the way you expect it to. Other times, you want an “official” debugging instrument that lives on as a permanent display in your game’s debugging interface.

How does one go about building a debugging system, however? In this blog tutorial, we’ll build a debugging system in the Godot game engine, one that is flexible, yet powerful. It’s useful both for temporary debugging and a long-term debugging solution. I use this system to provide a debugging interface for my own games, and I want to share how to make one like it in the hopes that it helps you in your own game development efforts.

This will be a multiple-part series. At the end of it, you’ll have the root implementation of the debugging system, and knowledge on how to extend it to suit your debugging purposes.

If you want to see the end result, you can download the sample project from Github: https://github.com/Jantho1990/Sample-Project-Debug-Interface

Existing Debugging Tools in Godot

Godot comes with a number of functionalities that are useful for debugging. The most obvious of these is the trusty print() function. Feed it a string, and that string will get printed out to the debugging console during game runtime. Even when you have a debugging system in place, print() is still useful as part of your toolset for temporary debugging solutions. That said, nothing you show with print() is exposed to an in-game interface, so it’s not very useful if you want to show debugging information on a more permanent basis. Also, if you need to see information that updates on every frame step, the debugging console will quickly be overwhelmed with a flood of printed messages, to the point where Godot will bark at you about printing too many messages. Thus, while print() definitely has its uses, we are still in need of something more robust for long-term debugging solutions.

One way I solved this problem in the past is by creating a DebugLabel node, based on a simple Label. This node would listen for a signal, and when said signal was received it would set its text value to whatever string was sent to it. The code looked something like this:


# DebugLabel
extends Label
export(String) var debug_name = "DebugLabel1"

func _ready() -> void:
  GlobalMessenger.listen(debug_name, self, "on_Debug_message_received")

func _on_Debug_message_received(data):
  text = str(data)

This solution also depended on a separate GlobalMessenger system that functions as a global way of passing information. But that system is a tale for another day.

This gave me a solution for printing debugging information that updated on every process step, without overloading the debugging console. While this little component was useful, it had its drawbacks. Every call to print a message to the DebugLabel would overwrite the previous value, so if I needed to show more than one piece of updating information, I would have to create multiple DebugLabel nodes. It wouldn’t take long for my scenes to be cluttered with DebugLabel nodes. Also, this still wasn’t part of a debugging system. If there was a DebugLabel, it’d show, regardless of whether you needed to view debugging information or not. Thus, while this node also served a valuable purpose, it was not enough for a proper debugging solution.

So what does a debugging solution need? It needs a way to conditionally show and hide debugging information, depending on whether such information needs to be viewed. It also needs to expose a method for game code to interact with it to pass in debugging information. There are many possible kinds of information that we’d want to see, so this interaction method must support being able to accept multiple kinds of information. Finally, there should be an easy way of creating debugging scenes to organize the information in whatever ways make sense to those that view the debugging information.

With that high-level information, let’s start by tackling the first part of that paragraph: conditionally showing and hiding the debugging information.

Creating a Test Scene

But before we start working on the debugging system proper, we should have a test scene that exists to help us test that what we’re creating actually works. It doesn’t need to be anything fancy.

While this part of the tutorial is optional, the tutorial series will be assuming the existence of this test scene. If you choose not to make it, then you’ll have to figure out how to test the debugging system’s code in a different way.

Create a scene, and have it extend Node. Let’s call it “TestScene”. In TestScene, add a Line2D node, make it whatever color you want (I chose red), and set the points to make it some easily-visible size (I set mine to [-25, 0], [25, 0] to make a 50px-long horizontal line). Move the Line2D somewhere near the center of the scene; it doesn’t have to be exact, as long as it isn’t too close to the top or edge of the game window. Finally, click the triangle button to run Godot’s main scene; since we don’t have one defined, Godot will pop up an interface that will allow you to make TestScene the default scene, which you should do.

You can alternatively just run this individual scene without making it the main scene; I have chosen to make it the main scene in this tutorial purely out of convenience.

This is what my version of the test scene looks like after doing these things:

Now that we have a test scene, let’s get to building the debugging system proper.

Creating the DebugLayer Global

We need a way to interact with the debugging interface from anywhere in our game code. The most effective way to do this is to create a global scene that it loaded as part of the AutoLoads. This way, any time our game or a scene in our game is run, the debugging system will always be available.

Start by creating a new scene, called DebugLayer, and have it extend the CanvasLayer node. Once the scene is created, go to the CanvasLayer node properties and set the layer property to 128.

That layer property tells Godot what order it should render CanvasLayer nodes in. The highest value allowed for that property is 128, and since we want debugging information to be rendered atop all other information, that’s what we’ll set our DebugLayer to.

For more information on how CanvasLayer works, you can read this documentation page.

Now, create a script for our node, DebugLayer.gd. For now, we’re not going to add anything to it, we just want the script to be created. Make sure it, as well as the DebugLayer scene, are saved to the directory _debug (which doesn’t exist yet, so you’ll need to create it).

Finally, go to Project -> Project Settings -> AutoLoad, and add the DebugLayer scene (not the DebugLayer.gd script) as an AutoLoad, shortening its name to Debug in the process. This is how we’ll make our debugging interface accessible from all parts of our game.

Yes, you can add scenes to AutoLoad, not just scripts. I actually discovered that thanks to a GDQuest tutorial on their Quest system, and have since used that pattern for a wide variety of purposes, including my debugging system.

To verify that our DebugLayer shows in our game, add a Label child to the DebugLayer scene, then run the game. You should be able to see that Label when you run the game, proving that DebugLayer is being rendered over our TestScene.

Toggle Debug Visibility

This isn’t particularly useful yet, though. We want to control when we show the debugging information. A good way to do this is to designate a key (or combination of keys) that, when pressed, toggles the visibility of DebugLayer and any nodes it contains.

Open up the project settings again, and go to Input Map. In the textbox beside Action:, type “toggle_debug_interface” and press the Add button. Scrolling down to the bottom of the Input Map list will reveal our newly-added action at the bottom.

Now we need to assign some kind of input that will dispatch this toggle_debug_interface action. Clicking the + button will allow you to do this. For this tutorial, I’ve chosen to use Shift + ` as the combination of keys to press (Godot will show ` as QuoteLeft). Once this is done, go ahead and close the project settings window.

It’s time to start adding some code. Let’s go to DebugLayer.gd and add this code:


var show_debug_interface = false


func _ready():
  _set_ui_container_visibility(show_debug_interface)


func _set_ui_container_visibility(boolean):
  visible = boolean

Right away, the editor will show an error on the visible = boolean line. You can confirm the error is valid by running the project and seeing the game crash on that line, with the error The identifier "visible" isn't declared in the current scope. That’s because CanvasLayer doesn’t inherit from the CanvasItem node, so it doesn’t contain a visible property. Therefore, we’ll need to add a node based on Control that acts as a UI container, and it is this node that we’ll toggle visibility for.

CanvasItem is the node all 2D and Control (aka UI) nodes inherit from.

Add a MarginContainer node to DebugLayer, calling it DebugUIContainer. Then, move the test label we created earlier to be a child of the DebugUIContainer. Finally, in DebugLayer.gd, change the visibility target to our new UI container:


onready var _uiContainer = $DebugUIContainer


func _set_ui_container_visibility(boolean):
  _uiContainer.visible = boolean

You may notice that I’m prefixing _uiContainer with an underscore. This is a generally-accepted Godot best practice for identifying class members that are intended to be private, and thus should not be accessed by code outside of that class. I also use camelCase to indicate that this particular variable represents a node. Both are my personal preferences, based on other best practices I’ve observed, and you do not need to adhere to this style of nomenclature for the code to work.

At this point, if you run the test scene, the test label that we’ve added should no longer be visible (because we’ve defaulted visibility to false). That’s only half the battle, of course; we still need to add the actual visibility toggling functionality. Let’s do so now:


func _input(_event):
  if Input.is_action_just_pressed('toggle_debug_interface'):
    show_debug_interface = !show_debug_interface
    _set_ui_container_visibility(show_debug_interface)

_input() is a function Godot runs whenever it detects an input action being dispatched. We’re using it to check if the input action is our toggle_debug_interface action (run in response to our debug key combination we defined earlier). If it is our toggle_debug_interface action, then we invert the value of show_debug_interface and call _set_ui_container_visibility with the new value.

Technically, we could just call the visibility function with the inverted value, but setting a variable exposes to outside code when the debug interface is being shown. While this tutorial is not going to show external code making use of that, it seems a useful enough functionality that we’re going to include it nonetheless.

Run the test scene again, and press Shift + `. This should now reveal our test label within DebugLayer, and prove that we can toggle the debug interface’s visibility. If that’s what happens, congratulations! If not, review the tutorial to try and identify what your implementation did incorrectly.

Congratulations!

We now have the basics of a debugging interface. Namely, we have a DebugLayer scene that will house our debugging information, one that we can make visible or invisible at the press of a couple of keys.

That said, we still don’t have a way of actually adding debugging information. As outlined earlier, we want to be able to implement debugging displays that we can easily reuse, with a simple API for our game code to send debugging information to the debugging system.

To accomplish these objectives, we’ll create something that I call “debug widgets”. How does a debug widget work? Find out in the next part of this tutorial!

You can review the end state of Part 1 in the Github repo by checking out the tutorial-part-1 branch.

Creating a Global Signal System in Godot

If you’ve worked in Godot long enough, you’ll have encountered the signal pattern. This pattern has one part of the code providing signals that it emits, which other parts of the code can listen for and react to. It’s very useful for keeping the parts of your codebase separated by concern, thereby preventing explicit dependencies, while still allowing for communication between different systems. This pattern is commonly used with UI elements, which have no bearing on how the game systems work, but still need to know when certain events happen. With signals, the UI nodes can listen for when specific game events occur, then take the data from those signals and use it to update their visuals.

In Godot, this pattern is implemented through the use of a Node’s signal keyword, emit_signal and connect methods, and a callback function. Example follows:


# Some example node script
extends Node
signal an_awesome_signal

func an_awesome_function():
  emit_signal('an_awesome_signal', 'data that the listeners receive')

func another_awesome_function():
  connect('an_awesome_signal', self, '_on_An_awesome_signal')

func _on_An_awesome_signal(data):
  print(data) # 'data that the listeners receive'

It is considered good Godot practice to name your listener callbacks after the signal they are responding to, prefixed with _on_ and with the first letter of the signal name capitalized.

Of course, you don’t have to just connect to signals within your node. Any node that is in the same scene as another node can connect to that node’s signals and listen for them. As explained, connecting nodes to one another allows for coding systems that need to respond to certain game events, but without having to call externalNode.external_node_method() each time external_node_method needs to be run in response to something happening.

Godot’s signal implementation is great, but there is a caveat: it doesn’t provide a clean way to listen for nodes which exist outside of the current scene. Let’s go back to the UI example. UI nodes and their code are usually kept separate from game systems code (after all, game systems shouldn’t need to manage the UI), often by creating entire scenes which house a portion of some UI widget, like a health bar. But how does said health bar know when it needs to be updated? Given this health bar (let’s call it HealthBarUI) is separate from the systems which actually calculate an entity’s health, we can’t directly connect it to the health system.

One way to solve this problem is to use relative paths when connecting the signals, e.g. ../../../HealthBarUI. Obviously, this solution is very brittle. If you decide that HealthBarUI needs to be moved anywhere in the node tree, you’ll have to update the signal connection path accordingly. It’s not hard to imagine this becoming untenable when adding many nodes which are connected to other nodes outside of their scene tree; it’s a maintenance nightmare.

A better solution would be to create a global singleton which your nodes can connect to, instead, adding it to the global AutoLoads. This alleviates the burden of relative paths by providing a global singleton variable that is guaranteed to be accessible from every scene.

Many programmers will advise against using the Singleton pattern, as creating so-called “god objects” is an easy way to create messy, disorganized code that makes code reuse more difficult. I share this concern, but advocate that there are certain times where you want to have a global singleton, and I consider this one of them. As with all practices and patterns, use your best judgment when it comes to determining how to apply them to solve your systems design problems.

GDQuest gives a good example of this pattern in this article. Basically, for every signal which needs to be globally connected, you add that signal definition to the global singleton, connect your local nodes to the singleton, and call Singleton.emit_signal() whenever you need to emit the global version of that signal. While this pattern works, it obviously gets more complex with each signal that you need to add. It also creates a hard dependency on the singleton, which makes it harder to reuse your nodes in other places without the global singleton.

I would like to propose a different take on the global singleton solution. Instead of explicitly defining global signals inside of a globally-accessible singleton file, we can dynamically add signals and connectors to a GlobalSignal node through add_emitter and add_listener methods. Once a signal is registered, then whenever it is emitted by its node, any registered listeners of that signal will receive it and be able to respond to it, exactly the same as how signals normally work. We avoid a hard dependency on the GlobalSignal singleton because we’re just emitting signals the normal way. It’s a clean solution that takes advantage of how Godot’s signals work as much as possible.

Intrigued? Let me show you how it works.

If you want to skip to the final result, you can access the sample project here: https://github.com/Jantho1990/Godot-Global-Signal.

Building the Basics

Let’s start by creating the file (I’m assuming you’ll have created a Godot project to work with, first). I’ve called it global_signal.gd. It should extend the basic Node. Once the file is created, we should add it to the global AutoLoads by going into Godot’s project settings, clicking the AutoLoad tab, then adding our script (with the variable name GlobalSignal).

This is how we will make GlobalSignal accessible from any scene’s code. Godot automatically loads any scripts in the AutoLoad section first and places them at the top of the game’s node hierarchy, where they can be accessed by their name.

With that out of the way, let’s start adding code to global_signal.gd. First, we need a way for GlobalSignal to know when a node has a signal that can be emitted globally. Let’s call these nodes emitters. This code should take care of adding emitters for GlobalSignal to keep track of:


# Keeps track of what signal emitters have been registered.
var _emitters = {}


# Register a signal with GlobalSignal, making it accessible to global listeners.
func add_emitter(signal_name: String, emitter: Object) -> void:
  var emitter_data = { 'object': emitter, 'object_id': emitter.get_instance_id() }
  if not _emitters.has(signal_name):
    _emitters[signal_name] = {}
  _emitters[signal_name][emitter.get_instance_id()] = emitter_data

Nothing too complex about this. We create a dictionary to store data for the emitter being added, check to see if we have an existing place to store signals with this name (and create a new dictionary to house them if not), then add it to the _emitters dictionary, storing it by signal name and instance id (the latter being a guaranteed unique key that is already part of the node, something we’ll be taking advantage of later).

We can now register emitters, but we also need a way to register listener nodes. After all, what’s the point of having a global signal if nothing can respond to it? The code for adding listeners is nearly identical to the code for adding emitters; we’re just storing things in a _listeners variable instead of _emitters.


# Keeps track of what listeners have been registered.
var _listeners = {}

# Adds a new global listener.
func add_listener(signal_name: String, listener: Object, method: String) -> void:
  var listener_data = { 'object': listener, 'object_id': listener.get_instance_id(), 'method': method }
  if not _listeners.has(signal_name):
    _listeners[signal_name] = {}
  _listeners[signal_name][listener.get_instance_id()] = listener_data

With that, we now have the ability to add emitters and listeners. What we don’t yet possess is a way to connect these emitters and listeners together. Normally, when using signals, we’d have the listener node connect() to the emitter node, specifying whatever signal it wants to connect to and the callback function which should be invoked (as well as the node where this callback function resides). We need to replicate this functionality here, but how do we ensure that a new emitter gets connected to all current and future listeners, and vice versa?

Simply put, every time we add a new emitter, we need to loop through GlobalSignal‘s listeners, find the ones which want to connect with that emitter’s signal, and perform the connection. The same is true for when we add a new listener: when a new listener is added, we need to loop through the registered emitters, find the ones whose signal matches the one the listener wants to listen to, and perform the connection. To abstract this process, let’s create a couple of functions to take care of this for us.


# Connect an emitter to existing listeners of its signal.
func _connect_emitter_to_listeners(signal_name: String, emitter: Object) -> void:
  var listeners = _listeners[signal_name]
  for listener in listeners.values():
    emitter.connect(signal_name, listener.object, listener.method)


# Connect a listener to emitters who emit the signal it's listening for.
func _connect_listener_to_emitters(signal_name: String, listener: Object, method: String) -> void:
  var emitters = _emitters[signal_name]
  for emitter in emitters.values():
    emitter.object.connect(signal_name, listener, method)

Now we need to modify our existing add functions to run these connector functions.


func add_emitter(signal_name: String, emitter: Object) -> void:
  # ...existing code

  if _listeners.has(signal_name):
    _connect_emitter_to_listeners(signal_name, emitter)


func add_listener(signal_name: String, listener: Object, method: String) -> void:
  # ...existing code

  if _emitters.has(signal_name):
    _connect_listener_to_emitters(signal_name, listener, method)

We first check to make sure an emitter/listener has already been defined before we try to connect to it. Godot doesn’t like it when you try to run code on objects that don’t exist. 😛

With that, the last thing we need to finish the basic implementation is to add a way for removing emitters and listeners when they no longer need to be connected. We can implement such functionality thusly:


# Remove registered emitter and disconnect any listeners connected to it.
func remove_emitter(signal_name: String, emitter: Object) -> void:
  if not _emitters.has(signal_name): return
  if not _emitters[signal_name].has(emitter.get_instance_id()): return  
    
  _emitters[signal_name].erase(emitter.get_instance_id())
    
  if _listeners.has(signal_name):
    for listener in _listeners[signal_name].values():
      if emitter.is_connected(signal_name, listener.object, listener.method):
        emitter.disconnect(signal_name, listener.object, listener.method)


# Remove registered listener and disconnect it from any emitters it was listening to.
func remove_listener(signal_name: String, listener: Object, method: String) -> void:
  if not _listeners.has(signal_name): return
  if not _listeners[signal_name].has(listener.get_instance_id()): return  
    
  _listeners[signal_name].erase(listener.get_instance_id())
    
  if _emitters.has(signal_name):
    for emitter in _emitters[signal_name].values():
      if emitter.object.is_connected(signal_name, listener, method):
        emitter.object.disconnect(signal_name, listener, method)

As with the add functions, the remove functions are both almost identical. We take an emitter (or listener), verify that it exists in our stored collection, and erase it from the collection. After that, we check to see if anything was connected to the thing being removed, and if so we go through all such connections and remove them.

That’s it for the basic implementation! We now have a functional GlobalSignal singleton that we can use to connect emitters and listeners dynamically, whenever we need to.

A Simple Test

Let’s create a simple test to verify that all this is working as intended.

This simple test is included in the sample project.

First, create a Node-based scene in your project. Then, add a LineEdit node and a Label node (along with whatever other Control nodes you want to add to make it appear the way you want), and create the following scripts to attach to them:


# TestLabel
extends Label


func _ready():
  GlobalSignal.add_listener('text_updated', self, '_on_Text_updated')


func _on_Text_updated(text_value: String):
  text = text_value


# TestLineEdit
extends LineEdit

signal text_updated(text_value)


func _ready():
  GlobalSignal.add_emitter('text_updated', self)
  connect('text_changed', self, '_on_Text_changed')


func _on_Text_changed(_value):
  emit_signal('text_updated', text)

You could also use the value argument for _on_Text_changed, instead of taking the direct value of text. It’s a matter of preference.

Assuming you’ve implemented the code from this tutorial correctly, when you run the scene, you should be able to type in the LineEdit node and see the values of the Label node update automatically. If it’s working, congratulations! If not, go back and look through the code samples to see what you might’ve missed, or download the sample project to compare it with yours.

Now, obviously, this is a contrived example. GlobalSignal would be overkill for solving such a simple scenario as the one presented in the test case. Hopefully, though, it illustrates how this approach would be useful for more complex scenarios, such as the HealthBarUI example described earlier. By making our global signal definition dynamic, we avoid having to make updates to GlobalSignal every time we need to add a new globally-accessible signal. We emit signals from the nodes, as you do normally; we just added a way for that signal to be listened to by nodes outside of the node’s scene tree. It’s powerful, flexible, and clean.

Resolving Edge Cases and Bugs

There are some hidden issues that we need to address, however. Let’s take a look at them and see how we can fix them.

Dealing with Destroyed Nodes

Let’s ask ourselves a hypothetical question: what would happen if a registered emitter or listener is destroyed? Say the node is freed by the parent (or the parent itself is freed). Would GlobalSignal know this node no longer exists? The answer is no, it wouldn’t. Subsequently, what would happen if we’re looping through our registered emitters/listeners and we try to access the destroyed node? Godot gets unhappy with us, and crashes.

How do we fix this? There are two approaches we could take:

  • We could poll our dictionaries of registered emitters and listeners every so often (say, once a second) to check and see if there’s any dead nodes, and remove any we find.
  • Alternatively, we could run that same check and destroy whenever we make a call to a function which needs to loop through the lists of emitters and listeners.

Of those two options, I prefer the latter. By only running the check when we explicitly need to loop through our emitters and listeners, we avoid needlessly running the check and thereby introducing additional processing time when we don’t know that it’s necessary (which is what would happen if we went with polling). Thus, we’re going to implement this only-when-necessary check in the four places that need it: namely, whenever we add or remove an emitter or listener.

There is an argument to be made that running the check as part of adding/removing emitters/listeners adds additional processing time when performing these functions. That’s true, but in practice I’ve found that the added time isn’t noticeable. That said, if your game is constantly creating and destroying nodes that need to be globally listened to, and it’s measurably impacting game performance, it may prove better to implement a poll-based solution. I’m just not going to do it as part of this tutorial.

First, let’s create a function that will both perform the check and remove the emitter/listener if it is determined it no longer exists.


# Checks stored listener or emitter data to see if it should be removed from its group, and purges if so.
# Returns true if the listener or emitter was purged, and false if it wasn't.
func _process_purge(data: Dictionary, group: Dictionary) -> bool:
  var object_exists = !!weakref(data.object).get_ref() and is_instance_valid(data.object)
  
  if !object_exists or data.object.get_instance_id() != data.object_id:
    group.erase(data.object_id)
    return true
  return false

First, we check all the possible ways that indicate that a node (or object, which is what a node is based on) no longer exists. weakref() checks to see if the object only exists by reference (aka has been destroyed and is pending removal from memory), and is_instance_valid is a built in Godot method that returns whether Godot thinks the instance no longer exists. I’ve found that I’ve needed both checks to verify whether or not the object truly exists.

You may want to abstract this object existence check into some kind of helper function that is made globally accessible. This is what I’ve done in my own implementation of GlobalSignal, but I chose to include it directly in this tutorial to avoid having to create another file exclusively to house that helper.

Even if we prove the object exists, we still need to check to make sure the stored instance id for the emitter/listener matches the current instance id of said object. If they don’t match, then it means the stored object is no longer the same as the one we registered (aka the reference to it changed).

If the object doesn’t exist, or if it’s not the same object as the one we registered, then we need to remove it from our dictionary. group is the collection we passed in for validation (this will be explained in more detail momentarily), and group.erase(data.object_id) deletes whatever value is stored at the key with the same name as data.object_id. If we’ve reached this point, we then return true. If we didn’t erase the object, we return false.

With our purge function defined, let’s go ahead and modify our add and remove functions to implement it:


func _connect_emitter_to_listeners(signal_name: String, emitter: Object) -> void:
  var listeners = _listeners[signal_name]
  for listener in listeners.values():
    if _process_purge(listener, listeners):
      continue
    emitter.connect(signal_name, listener.object, listener.method)


func _connect_listener_to_emitters(signal_name: String, listener: Object, method: String) -> void:
  var emitters = _emitters[signal_name]
  for emitter in emitters.values():
    if _process_purge(emitter, emitters):
      continue
    emitter.object.connect(signal_name, listener, method)


func remove_emitter(signal_name: String, emitter: Object) -> void:
  # ...existing code
    
  if _listeners.has(signal_name):
    for listener in _listeners[signal_name].values():
      if _process_purge(listener, _listeners[signal_name]):
        continue
      if emitter.is_connected(signal_name, listener.object, listener.method):
        emitter.disconnect(signal_name, listener.object, listener.method)


func remove_listener(signal_name: String, listener: Object, method: String) -> void:
  # ...existing code
    
  if _emitters.has(signal_name):
    for emitter in _emitters[signal_name].values():
      if _process_purge(emitter, _emitters[signal_name]):
        continue
      if emitter.object.is_connected(signal_name, listener, method):
        emitter.object.disconnect(signal_name, listener, method)

For each function, the only thing we’ve changed is adding the _process_purge() check before doing anything else with the emitters/listeners. Let’s examine what’s happening in _connect_emitter_to_listeners(), to detail the logic.

As we start looping through our dictionary of listeners (grouped by signal_name), we first call _process_purge(listener, listeners) in an if statement. From examining the code, listener is the current listener node (aka the object we want to verify exists) and listeners is the group of listeners for a particular signal_name. If _process_purge() returns true, that means the listener did not exist, so we continue to move on to the next stored listener. If _process_purge() returns false, then the listener does exist, and we can proceed with connecting the emitter to the listener.

The same thing happens for the other three functions, just with different values passed into _process_purge(), so I shan’t dissect them further. Hopefully, the examination of what happens in _connect_emitter_to_listeners() should make it clear how things work.

That’s one issue down. Let’s move on to the last issue that needs to be addressed before we can declare GlobalSignal complete.

Accessing an Emitter/Listener Before It’s Ready

Here’s another scenario to consider: what happens if we want to emit a globally-accessible signal during the _ready() call? You can try this out yourself by adding this line of code to TestLineEdit.gd, right after defining the global signal:


GlobalSignal.add_emitter('text_updated', self)
emit_signal('text_updated', 'text in _ready()')

We’d expect that, on starting our scene, our Label node should have the text set to “text in _ready()”. In practice, however, nothing happens. Why, though? We’ve established that we can use GlobalSignal to listen for nodes, so why doesn’t the connection in Label seem to be working?

To answer this question, let’s talk a little about Godot’s initialization process. When a scene is added to a scene tree (whether that be the root scene tree or a parent’s scene tree), the _ready() function is called on the lowermost child nodes, followed by the _ready() functions of the parents of those children, and so on and so forth. For sibling children (aka child nodes sharing the same parent), Godot calls them in tree order; in other words, Child 1 runs before Child 2. In our scene tree composition for the sample project, the LineEdit node comes before the Label node, which means the _ready() function in LineEdit runs first. Since Label is registering the global listener in its _ready() function, and that function is running after LineEdit‘s _ready() function, our text_updated signal gets emitted before the listener in Label is registered. In other words, the signal is being emitted too early.

How do we fix this? In our contrived example, we could move the Label to appear before the LineEdit, but then that changes where the two nodes are being rendered. Besides, basing things on _ready() order isn’t ideal. In the case where we want nodes in different scenes to listen for their signals, we can hardly keep track of when those nodes run their _ready() function, at least not without some complex mapping of your scene hierarchy that is painful to maintain.

The best to solve this problem is to provide some way to guarantee that, when emit_signal is called, that both the emitter and any listeners of it are ready to go. We’ll do this by adding a function called emit_signal_when_ready() which we call whenever we need to emit a signal and guarantee that any listeners for it that have been defined in _ready() functions are registered.

Unfortunately, we can’t override the existing emit_signal function itself to do this, because emit_signal uses variadic arguments (aka the ability to define any number of arguments to the function), which is something Godot does not allow for user-created functions. Therefore, we need to create a separate function for this.

We’ll need to add more than just the emit_signal_when_ready() function itself to make this functionality work, so I’ll go ahead and show all of the code which needs to be added, and then cover what’s going on in detail.


# Queue used for signals emitted with emit_signal_when_ready.
var _emit_queue = []

# Is false until after _ready() has been run.
var _gs_ready = false


# We only run this once, to process the _emit_queue. We disable processing afterwards.
func _process(_delta):
  if not _gs_ready:
    _make_ready()
    set_process(false)
    set_physics_process(false)


# Execute the ready process and initiate processing the emit queue.
func _make_ready() -> void:
  _gs_ready = true
  _process_emit_queue()


# Emits any queued signal emissions, then clears the emit queue.
func _process_emit_queue() -> void:
  for emitted_signal in _emit_queue:
    emitted_signal.args.push_front(emitted_signal.signal_name)
    emitted_signal.emitter.callv('emit_signal', emitted_signal.args)
  _emit_queue = []


# A variant of emit_signal that defers emitting the signal until the first physics process step.
# Useful when you want to emit a global signal during a _ready function and guarantee the emitter and listener are ready.
func emit_signal_when_ready(signal_name: String, args: Array, emitter: Object) -> void:
  if not _emitters.has(signal_name):
    push_error('GlobalSignal.emit_signal_when_ready: Signal is not registered with GlobalSignal (' + signal_name + ').')
    return
  
  if not _gs_ready:
    _emit_queue.push_back({ 'signal_name': signal_name, 'args': args, 'emitter': emitter })
  else:
    # GlobalSignal is ready, so just call emit_signal with the provided args.
    args.push_front(signal_name)
    emitter.callv('emit_signal', args)

That’s quite a lot to take in, so let’s break it down, starting with the two class members being added, _emit_queue and _gs_ready.

_emit_queue is a simple array that we’re going to use to keep track of any signals that have been marked as needing to be emitted when GlobalSignal decides everything is ready to go. _gs_ready is a variable that will be used to communicate when GlobalSignal considers everything ready.

I use _gs_ready instead of _ready to avoid giving a variable the same name as a class function. While I’ve found that Godot does allow you to do that, I consider it bad practice to have variables with the same name as functions; it’s confusing, and confusing code is hard to understand.

Next, let’s examine our call to _process() (a built-in Godot process that runs on every frame update):


# We only run this once, to process the _emit_queue. We disable processing afterwards.
func _process(_delta):
  if not _gs_ready:
    _make_ready()
    set_process(false)
    set_physics_process(false)

If _gs_ready is false (which is what we’ve defaulted it to), then we call _make_ready() and subsequently disable the process and physics process update steps. Since GlobalSignal doesn’t need to be run on updates, we can save processing time by disabling them once we’ve run _process() the first time. Additionally, since GlobalSignal is an AutoLoad, this _process() will be run shortly after the entire scene tree is loaded and ready to go.

Let’s check out what _make_ready() does:


# Execute the ready process and initiate processing the emit queue.
func _make_ready() -> void:
  _gs_ready = true
  _process_emit_queue()

The function sets _gs_ready to true, then calls _process_emit_queue(). By marking _gs_ready as true, it signals that GlobalSignal now considers things to be ready to go.

Moving on to _process_emit_queue():


# Emits any queued signal emissions, then clears the emit queue.
func _process_emit_queue() -> void:
  for emitted_signal in _emit_queue:
    emitted_signal.args.push_front(emitted_signal.signal_name)
    emitted_signal.emitter.callv('emit_signal', emitted_signal.args)
  _emit_queue = []

Here, we loop through the _emit_queue array, push the signal name to the front of the arguments array, and use callv to manually call the emit_signal() function on the emitter node, passing in the array of arguments (emit_signal() takes the signal’s name as the first argument, which is why we needed to make the signal name the first member of the arguments array) . When we’ve gone through all of the members of _emit_queue, we reset it to an empty array.

Finally, we come to the emit_signal_when_ready() function, itself:


# A variant of emit_signal that defers emitting the signal until the first process step.
# Useful when you want to emit a global signal during a _ready function and guarantee the emitter and listener are ready.
func emit_signal_when_ready(signal_name: String, args: Array, emitter: Object) -> void:
  if not _emitters.has(signal_name):
    push_error('GlobalSignal.emit_signal_when_ready: Signal is not registered with GlobalSignal (' + signal_name + ').')
    return
  
  if not _gs_ready:
    _emit_queue.push_back({ 'signal_name': signal_name, 'args': args, 'emitter': emitter })
  else:
    # GlobalSignal is ready, so just call emit_signal with the provided args.
    args.push_front(signal_name)
    emitter.callv('emit_signal', args)

First, we check to see if the signal we want to emit has been registered with GlobalSignal, and return early if it is not (with an error pushed to Godot’s console to tell us this scenario happened). Our next action depends on the value of _gs_ready. If it’s false (aka we aren’t ready), then we add a new entry to _emit_queue and pass in the signal name, arguments, and emitter node, all of which will be utilized during _process_emit_queue(). If it’s true, then we called this function after everything has been marked as ready; in that case, there’s no point in adding this to the emit queue, so we’ll just invoke emit_signal() and call it a day.

With that, GlobalSignal should now be able to handle dispatching signals and guaranteeing that the listeners defined during _ready() functions are registered. Let’s test this by changing our modification to TestLineEdit so it uses emit_signal_when_ready():


GlobalSignal.add_emitter('text_updated', self)
GlobalSignal.emit_signal_when_ready('text_updated', ['text in _ready()'], self)

Note that we need to convert our ‘text in _ready()’ argument to be wrapped in an array, since we need to pass an array of arguments to the function.

Also note that we have to pass in the emitter node, since we have to store that in order to call emit_signal() on it later.

If, when you run the scene, the Label node shows our text string, that means our changes worked! Now we can declare GlobalSignal done!

Using Global Signals

Congratulations! You now have a dynamic way to define globally-accessible signals that closely follows Godot’s natural signals implementation. Adding new global signals is easy, and doesn’t involve changing the GlobalSignal singleton at all.

At this point, you might wonder, “Why not convert all of my signals to be global signals?” That’s not necessarily a great idea. Most of the time, we want to keep our signals local, as when we start connecting code from disparate parts of our code base it can make it confusing to recall which things are connected to what. By keeping signals local whenever possible, we make dependencies clearer and make it harder to design bad code.

That’s one of the things I actually like about this approach to implementing global signals. We’re still emitting signals locally; we just choose which signals need to also be exposed globally. You can connect signals locally and globally, with the same signal definitions.

What are some good use cases? UI nodes, as mentioned before, are a great example of a good use case for this pattern. An achievements system needing to know when certain events occur is another possible use case. Generally, this pattern is best suited for when you have multiple “major” systems that need to talk to one another in an agnostic manner, while local signal connections are better for communication between the individual parts of a major system.

As with any pattern or best practice, GlobalSignal should be carefully considered as one of many solutions to your problem, and chosen if it proves to be the best fit.

One last time, here is the link to the sample project, if you didn’t build along with the tutorial, or just want something to compare your implementation against.

Hopefully, this approach to global signals helps you in your projects. I’ve certainly made great use of it in mine!

Sample Project: Star-Rating (Vue)

This is part two of a multi-part series about React and Vue.
Part 1: React
Part 2: Vue

As I have been learning React lately, I’ve also been learning Vue. I currently use Vue at work, but I wanted to make a sample project for myself. I’ve been interested in doing a comparison between React and Vue, so I’ve chosen to build a Vue version of the Star-Rating app I built in React. The app is small and simple: you have a rating represented as a line of stars, and you can change how the star rating is calculated via an inputs component.

For the final result, click this link.

This tutorial presumes a basic knowledge of HTML, CSS, and JS, along with some basic knowledge of how to develop using a command line interface (or CLI) and how to use Node Package Manager (or NPM). If you’re unfamiliar with any of these things, feel free to make an online search for whatever you don’t know.

Vue makes some use of ES6 syntax, and I try to write my code using ES6 as well. I’ll try to link to definitions of ES6 syntax when I initially use them, but I won’t take especial time to explain them in-depth. If you want a quick overview of ES6, you can read this.

Table of Contents

Setting Up
Looking Around
Creating Our First Component
Rendering Stars with FontAwesome
Calculating How Many Stars to Render
Rendering the Stars
Vue Slots
Finishing the First Component
Introducing a Second Component
Rendering the Input
Adding the Other Inputs
Better Validation
Final Touches
Conclusion

Setting Up #

For this tutorial, I chose to use vue-cli, a command-line tool that will set up a boilerplate Vue single-page application, similar to create-react-app. If you don’t have it already, you can install it via npm install -g [email protected].

I’ve specified a specific version number because, as of this post, vue-cli V3 is in beta and will be released soon. It works differently from V2, so to follow this tutorial you’ll need a V2 version of the tool.

To set up a project, navigate in the terminal to the directory you want to make the project in (for me, this is ~/jdev/projects), and then enter this command:

vue init webpack star-rating-vue

Right away, you’ll be presented with a text interface that asks you a number of questions regarding how you want to set up your project. But, before we get to that, let’s look at the command we just entered. The first part is vue, calling the tool; the second part is init, which tells the tool to start up the project; and the fourth part, star-rating-vue, is the directory our app is going to be saved in. So what is the third part, webpack, for? The way vue-cli V2 works is by using pre-built templates, and which template you choose determines what kinds of goodies you start out with. In this case, we’re using the “webpack” template, which will set up a full Webpack-based project, as well as a few other things:

vue-loader
Allows us to write single-file components (we’ll get to that momentarily).
hot-reload
Allows us to see changes to our app on save.
linting
Helps enforce proper coding conventions by marking up your code editor.
testing
Sets up a unit testing framework. (We won’t be using tests in this tutorial.)
css extraction
Pulls our CSS out of single-file components (we’ll get to that momentarily, too).

If you look on the official page for vue-cli, you’ll find some other “official” templates you could use, instead. There are also third-party templates; just search online for “vue third-party templates” and you’ll be bound to find some.

Now that we’ve taken a brief overview of what templates are, let’s get back to the actual setup. The command line tool will present you with a set of options, one at a time. For most of the options, you’ll need to type in either y or n and hit enter to make your choice; some are multiple choice, and a few involve typing up strings.

I’ll go over each option briefly:

  • Project name: “name-of-your-project” (requires URL-friendly characters, so no spaces; also, no capital letters allowed)
  • Project description: “Whatever description you feel like including.”
  • Author: “Your Name <[email protected]>” (if you have a global Git user, this should be autofilled).</[email protected]>
  • Vue build: Here, you’ll have two options: Runtime + Compiler and Runtime-only.
    • Runtime + Compiler allows more flexibility in how you build your projects, such as passing in templates as a string (from an AJAX call, for example), but it makes the final build larger (by as much as 30%).
    • Runtime-only is a lighter build, and performs slightly faster, but imposes some restrictions on how you can set up your project.

    We shouldn’t need any functionality from the compiler for this project, so let’s select Runtime-only.

  • Install vue-router? vue-router is useful for setting up something resembling url navigation within an SPA. We won’t be doing that here, so you can choose n.
  • Use ESLint to lint your code? If you care about following style guides, choose y, and then pick the linter you want to use in the subsequent section. Otherwise, choose n.
  • Set up unit tests: As mentioned earlier, we won’t be setting up unit tests here, so choose n.
  • Setup e2e tests with Nightwatch? Nightwatch is a testing tool that allows you to render the app in full, then set up “scenarios” of user actions to test how your app acts under those circumstances. We will not be doing that here, so you can choose n.
  • Should we run `npm install` for you after the project has been created? Since we would need to do this anyway, go ahead and choose Yes, use NPM. If you prefer Yarn, you can choose that instead.

After that last choice, vue-cli will get to work setting up your app with the choices you’ve made. It may take a while, depending on your internet speed, but in a minute or two your boilerplate app should be generated and ready for you to work with. When it’s finished, start the development server by cd-ing into the project directory and typing npm run dev. Open a browser and navigate to localhost:8080; if you see the Vue logo and some links, the dev server is successfully running!

If you don’t want to use vue-cli, you can import Vue directly, via a CDN, locally-hosted script, or NPM install. Check out the official docs for more information. For this tutorial, however, I’ll be assuming you’re using vue-cli.

Looking Around #

Now that we’ve (finally) got the barebones project set up, open the project in your preferred code editor.

If you use VS Code, you can spawn a new editor window from the project directory by cd-ing into it, then typing code . (you may need to explicitly enable this if you use a Mac). Other code editors likely support this functionality as well, though the commands you use may be different.

You should see a number of directories and files already set up. Let’s look in the src directory and open up the file called main.js. It should look something like this:

This file sets up the root Vue instance. All the other components in our application will be kept under this root instance. We’re importing Vue itself, as well as a file called App (which we will be examining shortly). There’s a quick line disabling the production tip. Finally, we create a new Vue instance, and pass in a config object with two settings:

  • el: '#app' This tells Vue what element from the HTML document to bind the instance to. The original element will be replaced with our rendered application. (You can see this element by looking in the `index.html` file in the project root directory.)
  • render: h => h(App) This tells the instance’s render function what should be rendered, via passing in a callback function that handles the actual rendering. In this case, we’re rendering the App file imported at the top of the page.

Let’s dig into that second option a little bit. The code is using an ES6 arrow function, with a single argument: h. What’s h? It’s an alias for the createElement function, the letter itself short for “hyperscript” (you can check this Github issue comment for a deeper explanation of both the render prop and the meaning of h). h is then called, and App is passed in and rendered.

So what is App doing? Let’s find out:

Note the .vue extension. This is what Vue calls a Single File Component. A single file component allows you to write your HTML, JavaScript, and CSS for that component in the same file. For applications comprised of multiple, modular parts, this can be very useful for keeping markup, logic, and styles associated with a single component located in one place. We will be creating our own single file components (henceforth just “components”) soon enough, but first let’s dig into the default App component, section by section.

First, the HTML:

The entire section is wrapped in a template tag. This gives Vue access to the markup contained within; the tag itself will not be rendered. Next, you have a single div with an id of app. This is not the same app element that the root Vue instance replaced; it merely shares the same id. Finally, we have an image tag — the Vue logo — and a strange-looking, self-closing tag named HelloWorld. This is another component, imported into App and being rendered below the logo.

All elements in your component must be wrapped by a single container element; here, the #app div.

Next, in the javascript section:

This contains the JavaScript code for the component. First, you’ll see the HelloWorld component being imported from the components directory. Next, the code exports an object as default, containing two properties:

  • name The name assigned to this component. While not strictly required (except in a few cases), it’s good practice to always give your components names.
  • components This tells the component instance what custom components should be expected. This is how we can write <HelloWorld/> in our template and have Vue replace it with the template from the HelloWorld component.

Finally, there’s the styles section:

This is just regular ol’ CSS, styling the #app element. During development, these styles will be added to the <head>, but because our Webpack template includes CSS extraction, the styles in this section will get pulled out and added to a single CSS file during the build stage.

That does it for App. Now let’s look at the other component included in the boilerplate — the HelloWorld component.

As you may have noticed, the attributes for the a tags in your local file are split onto separate lines. This is considered by Vue’s style guide to be best practice. I’ve chosen to condense the tags back onto one line for my gist for the sake of brevity, but in general I agree with this philosophy.

It’s another single file component. The HTML template contains a couple unordered lists of links and some h2 tags titling them, as well as an h1 with {{ msg }}. If you’re looking at the app in the browser, however, the h1 says “Welcome to Your Vue.js App”. What’s going on?

Let’s look in the script section to find out:

There isn’t much here. We have the component’s name, “HelloWorld”. There are no components being passed in; however, we do have a method called data, which returns an object. This object contains one property: msg, whose value is set to “Welcome to Your Vue.js App”. Hey, that’s what the h1 says on the rendered page! That’s where it comes from. Anything you set in a Vue component’s data method becomes available in your template as a variable. You can output the values of those variables using a pair of curly braces.

Why is data a method returning an object, instead of just being a property set as an object? If you have multiple instances of the same component on a page, each component’s properties are shared with each other by reference. Thus, if data were an object, updating the data on one component would update that piece of data for every instance of that component. To avoid this, we just make data a method that returns a fresh object each time it is called.

The docs provide additional explanation, as well as an example.

Lastly, let’s take a look at the style section for this component:

Again, it’s just CSS, but this time there’s a scoped attribute in the style tag. This means the CSS in this component will only be applied to styles within that component. How does it do this? If you go to the web page and do a dev inspection on one of the links, you’ll notice there’s a data attribute attached with a long, random string (prefixed with data-v-) as its value. Vue sets the CSS rules in HelloWorld to target both the rule specified in the code and the value of this data attribute. Thus, the styles in this component will not affect anything outside of its scope.

You are still able to affect the element styles in this component from the outside, but be aware that, since the data attribute specification adds specificity, you may need to be more explicit with your rule declarations than usual to override the component’s styles.

Whew! For a boilerplate example, that was a lot of stuff to look over. Hopefully, this overview gave you a basic idea of how Vue works; the rest of what we need to know, we’ll pick up as we go along. Let’s start making our Star-Rating app by creating our first component!

Creating Our First Component #

In the “components” directory, create a new file and name it StarRating.vue. Here’s the structure we’ll need to start with:

In the template section, we’ve just added a single div with a class of “star-rating”, which will serve as the container element Vue requires. That’s all we’ll add, for the moment; we’ll get to adding stars shortly.

In the script section, we’re exporting a default object with two properties: name, and props. Props are pieces of data that a component expects to be passed down from a parent component. We’ll be setting up App to do that, but for now we’ll just set up defaults to work with. We’ll also add a type property to each prop, which allows Vue to detect when a prop gets passed down that doesn’t match the type specified, and warn you in the developer console.

The style section is currently empty. Eventually, we’ll put in the styles specific to the StarRating component, but first let’s have something to actually render.

Rendering Stars With FontAwesome #

If you read the React version of this sample project, you’ll remember that we chose an icon set called FontAwesome, imported some star icons into our app via a series of NPM libraries, and wrote some logic to control how to render those icons. We’re going to do the same thing here, but instead of using react-fontawesome, we’ll be using vue-fontawesome.

npm i --save npm i --save @fortawesome/fontawesome-svg-core @fortawesome/fontawesome-free-solid @fortawesome/fontawesome-free-regular @fortawesome/vue-fontawesome

To summarize, we’re installing Fontawesome, two sets of Fontawesome icons, and a package which provides FontAwesome Vue components. Once the packages have finished installing, we need to import them into our project.

First, in App.vue, replace the HelloWorld import with this:

The first statement is importing the main fontawesome library. The next two import statements are using multiple import syntax to extract specific parts of the fontawesome-free-solid and fontawesome-free-regular icon libraries. For the latter, we are also assigning aliases to the imports, since they share names with the solid counterparts. Finally, we use fontawesome.library.add() to make these four icons globally available for all our other components to use.

We could also have just imported all of a library’s icons — with, say, import solid from '@fortawesome/fontawesome-free-solid' — and added all the icons globally. If you know you only need a small set of icons, however, it’s best to just import the ones you need.

Next, let’s go back to StarRating and add one more import:

Similarly to how we imported parts of the icon libraries, we are importing parts of the @fortawesome/vue-fontawesome library — specifically, FontAwesomeIcon and FontAwesomeLayers. Soon, we’ll be using these to render our star ratings. First, though, we need to tell our StarRating component to make these FontAwesome components available to the template, and that’s what we’re doing by adding the components property to our exported object, its members FontAwesomeIcon and FontAwesomeLayers.

With our FontAwesomeIcon component imported, let’s go ahead and update our template:

Inside the container div, we’ve added a new tag: font-awesome-icon. This is our FontAwesomeIcon component, and when Vue renders our app it will take this component tag and replace it with the component’s template, just as the HelloWorld component is being rendered into our default application. We’ve given it a class of “star”, and we’ve also set a property named icon, its value set to “star” as well. This icon property is a prop we are passing down to the Vue component, telling it which icon we’d like it to render.

The official Vue style guide strongly recommends naming custom components using multiple words, as well as naming your components using either all Pascal-case (FirstLetterCapitalized) or kebab-case (dashes-between-words), and I agree with their reasons. For our tutorial, I’ve chosen to use kebab-case, but ultimately it’s just a matter of preference.

Let’s go ahead and render our new component into the App.vue component file:

We’ve added an import for our StarRating component before the other imports (imports are asynchronous, so JavaScript will not execute any code until all imports have been processed, which means we don’t have to import StarRating after our FontAwesome imports). We’ve also removed the import for HelloWorld because we won’t be needing it. In components, we replace HelloWorld with StarRating. Finally, in template, we again swap HelloWorld for star-rating. If all goes well, upon save you should see a single black star rendered just below the Vue logo.

Calculating How Many Stars to Render #

If you played around with the final result at the beginning of the tutorial, you may have noticed three different types of stars: “full” stars, “half” stars, and “empty” stars. This is how I’ve chosen to represent the star ratings for this project.

Before we can render any of that, though, we need to calculate four things:

  • What is the maximum number of stars we should render?
  • How many full stars should we render?
  • How many half stars should we render?
  • How many empty stars should we render?

You may be wondering why it’s necessary to calculate a “maxStars” value. Sure, we could simply calculate the full, half, and empty star values without calculating a separate max value, but I’ve found that it makes the math simpler to just figure out the maximum number ahead of time and use it in calculations later, so that is what I’ve chosen to do.

Let’s update the StarRating component with the necessary code to perform our calculations:

To briefly explain each calculation:

  • maxStars(): We take the maxRating and divide it by the starRatio, then round the answer up to the next integer.
  • fullStars(): We take the rating and divide it by the starRatio, then round the answer down to the next integer.
  • halfStars(): We take the modulus (or remainder) of rating and starRatio, get half the value of starRatio, and compare the two using a ternary operator. If `x` is greater than or equal to `i`, we return one half-star; otherwise, we return no half-stars.
  • emptyStars(): We take maxStars and subtract from it fullStars and halfStars.

Note where we have placed these methods. These are what Vue calls “computed properties”. In other words, when you access these properties, whether in your template or other parts of your code, Vue will return the result of the function you’ve assigned to that computed property.

If you examine the code used for the emptyStars method, you can see that we’re using the other three computed properties to calculate its return value. The other three computed properties, in turn, are calculated based on the value of the props passed in to the StarRating component. If any of the props should change, Vue will recalculate the values of these computed properties, and any rendered data using these computed properties will also be updated; it all happens automatically.

Rendering the Stars #

Let’s see the computed properties in action. Modify the template thusly:

What we’ve done is add a directive to our font-awesome-icon component called v-for. This directive is telling Vue to render a component this many times, just like looping through a list of items. The text inside of the v-for directive is actually specific syntax that Vue will interpret similarly to a for-in loop. Vue lets you enumerate objects and arrays with this syntax, as well as ranges (which is what we’re using in this case).

Be sure to check out Vue’s official documentation on this subject, which shows you more specifically how to work with arrays and objects using v-for.

We’ve also added a key attribute, and you might have noticed the colon in front of it. What is this colon for? This is a shortcut for the directive v-bind, which binds an HTML attribute to a JavaScript expression. The value of key equals "`fs${fs}`", which is a template literal expression. To use ES5, "fs" + fs (which would also work here; I just prefer using ES6 when possible). When Vue reads this, it will parse the expression bound to :key as actual JavaScript.

You can read more about attributes here. If you follow the link, you’ll notice that the Vue examples use v-bind: instead of just the colon. That is the “full” way to write out binding syntax, but Vue allows you to use just the colon as a shortcut.

Why do we need key? Vue strongly recommends that you add a unique key to each item rendered by a v-for loop because it helps Vue track each item in its virtual DOM, which ultimately improves performance. The expression set in key will generate a string with the incrementing value of the variable fs (set in the value of v-for), which is enough to make this key unique. Since, technically, it isn’t required, we could choose to leave the key out, but it’s good practice to always add a key attribute when using v-for.

So what does all this actually do? If you’ve saved this code, you might have already noticed that the demo app in the browser has already updated to show two black stars instead of one. This is because of our v-for directive, fs in fullStars. The default prop value for rating is 5, and our default starRatio is 2. Those two props are used by the fullStars computed property to calculate its value, and in this case the result is 2. v-for then loops n in 2, resulting in rendering the font-awesome-icon component twice. Again, this happens automatically. You don’t have to write a separate function to handle the looping and rendering for you. Vue just takes care of it because you told it to via the v-for directive.

Now that we know how this works, let’s go ahead and add the template code to render our half stars and our empty stars:

The process for rendering our half stars and empty stars is nearly identical to the process for rendering full stars. We just add a FontAwesome component and use v-for to render as many of each component as dictated by the related computed properties. There are a few differences, however, which I’ll go over briefly.

In both cases, we need to generate unique keys for the key attribute, and to accomplish this we just change the variable to match what we use in the v-for directive and alter the string prefix. For the empty stars, "`es${es}`", and for half stars, "`hs${hs}`".

For the empty stars icon component, instead of just passing in an icon attribute, we’re binding the attribute to a JavaScript array: ['far', 'star']. This is because of the font-awesome-icon component’s interface. By default, passing in a simple icon attribute will render an icon from the solid library of icons, which is what we used to render our full stars. For empty stars, however, we want to render a different icon; specifically, one from the regular library that looks like the empty outline of a star. In order to tell the font-awesome-icon component to render an icon from the regular library, FontAwesomeVue expects us to pass in an array with two members. The first item is the abbreviation of the library we want to pull the icon from, in this case far; the second item is the name of the icon we want, in this case star. Since we’re passing in an actual array, that means we need to bind icon so we can send the configuration array to the component.

You could use ['fas', 'star'] to render our full star icons, instead of relying on the component’s default assumption, if you prefer.

The most significant difference is with how we’re rendering the half stars. Instead of a single font-awesome-icon, we’re rendering two, and we’ve wrapped both components with a font-awesome-layers component. This is because FontAwesome 5 (the version this tutorial is using) doesn’t have a “half-filled” star icon; instead, it has a literal half of a star: one that is “filled” (from solid), and one that is “empty” (from regular).

To get the half-filled star effect we’re looking for, we need to combine those two icons into one. The official way to do this in FontAwesome is to use Layering; in vue-fontawesome, this means using the font-awesome-layers component. Anything within the font-awesome-layers tags will get combined into the same space and be shown as though it were one icon.

Thus, we’ve set the two font-awesome-icon components to use the half-star icons; additionally, we pass in a flip attribute to the empty half-star so it’s reversed. The end result is something that looks like a half-filled star. Excellent!

After I started writing this tutorial, FontAwesome did come out with a half-filled star icon, so we could use that instead of my original solution. I’m not going to change this solution, however, because it also serves to teach about Vue slots — which is coming right up!

Vue Slots #

Let’s take a moment to further examine font-awesome-layers. Up until now, we’ve been using self-closing tags for our components, because it’s simpler to read and we had no need to use opening/closing tag syntax. With font-awesome-layers, however, we’re not only using opening and closing tags, we’re including other components inside of the tags. This is using an aspect of Vue components called slots. To explain what slots do, let’s use a quick example:

The above code (assuming path names and whatnot) results in rendering the following:

In the first component, called ExampleComponent, we basically have two parts: an h2 with the text “These are my children:”, and a ul containing a slot tag. What is this slot tag for? This is telling Vue that anything which is slotted into this component should go here. What does slotting mean, though? To answer that, let’s look at the second component.

The second component is called ExampleParent. In it, we have an h1 with the text “I’m the Parent”. What comes next is our first component, example-component, but in between the opening and closing tags of example-component are three li tags, each containing a human name. If you look at the results image, you’ll see that it renders “I’m the Parent” (from ExampleParent), followed by “These are my children” (from ExampleComponent), and lastly by the names (from ExampleParent). Vue is taking the li tags we enclosed within the example-component tags and inserting them in place of the slot tag we put in ExampleComponent. That is slotting.

If you’d like to follow along, you can copy-paste the code snippets into your components directory as separate Vue files, and then import ExampleParent into App, just like we did with StarRating. Don’t forget to remove it when you’re done with it!

Now that we have a better idea of what slots are, let’s look at the font-awesome-layers component again:

We are slotting two font-awesome-vue icons inside of the font-awesome-layers component. That layers component will then render FontAwesome’s layers container (basically a div container and some classes), so we can make those two half-star pieces look like a half-filled star.

Finishing the First Component #

Alright, we’ve added the code necessary to render all of our stars: full, half, and empty. The last thing we’ll do (for now) is add some styling in the style tags at the bottom of the StarRating component. The finished* code should look like this:

Following the steps outlined so far, you should now see the Vue logo with two and a half gold stars rendered beneath it. If you do, congratulations on making it this far! If not, you should review your code and compare it with what I’ve posted. Hopefully that should expose your problem and get you back on the right track. If not, feel free to submit an issue in the project repository!

Did you notice the asterisk on “finished”? There’s one more thing we’re going to take care of later, but we don’t need to worry about it right now.

Assuming things are working, let’s go ahead and pass a prop down to our component. From App.vue:

When you save the file, you should see the rendered stars change. Assuming you passed in a “7”, like I did, there should be three and a half stars rendered — three full, one half, and one empty. Vue took the rating number we passed down to the StarRating component and converted it to the star rating we now see. We didn’t have to do anything other than pass down that prop.

Of course, at the moment we can only change the rating by manually editing our code to change the value of the rating prop. That obviously isn’t a very user-friendly way to change the star rating, so we’re going to want to have an interface for us to enter the prop values we want and have StarRating update in response, as shown in the final result. To do that, we need to build a second component specifically for handling this interface…a RatingInputs component!

Introducing a Second Component #

To begin, let’s create a new file in the components directory, and call it RatingInputs.vue. Add the following code:

First, in the template, we have a single input for the rating, as well as a label for that input, all wrapped in a container div. Then, in the script section, we have the component’s name and our prop definitions (like we had in the StarRating component). Finally, we have the empty style tags at the bottom.

Next, we’ll take advantage of Vue’s form input bindings and wire up our input element to the component’s data:

We’ve added a directive to the input, v-model, and set it equal to rating_. In the script section, we’ve added a data function, which returns an object containing one property: rating_ = this.rating. What this is doing is initializing rating_ with the value of rating, a prop this component expects to be passed down (and if it isn’t, then it defaults to 5, as we set up in the props section). Because we’ve told Vue to “model” the value of rating_ on our rating input, Vue sets the input element’s value equal to the value of rating_; when we change the value in the input element, Vue then updates rating_ to equal the new value. Thus, Vue gives us two-way data binding without having to do extra work.

Why not just set the input’s value to be rating directly? Technically, Vue would allow us to do that; however, this is considered bad practice. The reason is because anything defined as a prop will have its value changed any time a new value is passed down to the prop via the parent component. Thus, as best practice, if we want to modify a prop’s value from the child component, we should instead create a property in the data object and initialize it with the prop’s value, then modify that data property instead of the prop.

There is no established nomenclature for how you should name your props and data values in cases like these. I’ve chosen to use rating as the prop name for easier reading when passing it in from the parent, and rating_ because Vue doesn’t allow you to start data fields with underscores, and this seemed the next best thing.

Rendering the Input #

Although our RatingInputs component is wired up with the rating input, we still don’t have a way to tell the StarRating component what that input’s value is. To do that, we’re going to need to use Vue’s events system. We’ll start by updating our RatingInputs component:

We’ve added another new directive to the input element: v-on. This tells Vue to listen for an event (it can be a browser or custom event) emitted by this element. :input tells Vue which event to listen for, and "handleRating" is the callback we want Vue to run on this event. In the script section, we’ve added a new property to the component object, called methods. This is where you store any functions you want your component to have access to.

You should use regular functions here instead of arrow functions. Vue automatically handles binding a component’s this to its methods, but it can’t do so for arrow functions because those use the this value of the calling function — which, in this case, won’t be the RatingInputs component, but Vue’s handler for methods.

In methods, we’ve added one function: handleRating, which does two things: first, it gets the value for rating_ from the component via destructuring assignment; second, we call this.$emit, which is a special component function to emit a custom event that only a direct parent component will be able to read. This $emit function takes, as its arguments, a string “rating-update” and an object literal set to {rating: rating_}. rating-update is the name of our custom event, and the object literal is an argument that will be passed in to any callbacks the parent App component has listening for the rating-update event.

Now, in App, let’s make some changes to listen for our custom event:

In the template section, we’ve removed the hard-corded value for :rating and changed it to be the value of the rating data field, which we add in the script section. We’ve also added the rating prop to our RatingInputs component, as well as @rating-update, the custom event that we’re listening for, and a handleRatingUpdate callback.

@ is Vue shorthand syntax for v-on:

We’ve added a methods object to the App component object, as well as the function handleRatingUpdate. It takes one argument, data, which is equal to the object literal we passed into the $emit function in the RatingInputs component. From data, we extract the rating property, and then set this.rating (the data field in App) to equal rating. Upon saving the file, we should now be able to edit the value of rating input element and update the rendered stars in real-time. If you’re able to do so, congratulations!

Adding the Other Inputs #

While playing with the ratings input, you’ll likely notice that you can add a higher input than 10, and a lower input than 0. While this doesn’t seem to affect anything visually, this is not ideal behavior. We want to limit the input so that the user can’t add a higher rating than the maximum, or a lower rating than the minimum. Also, it would be nice to be able to change the star ratio, or how many stars get rendered per rating point (e.g. render one star per rating point, rather than one-half). Let’s get on that.

Let’s start by adding additional inputs for the values outlined above: maxRating, minRating, and starRatio. The code for both RatingInputs and App is as follows:

As you can see, the majority of we needed to do is replicate the code we added to wire up rating, changing only the names. We use the same handleRating callback to process any change to the inputs and pass them up to the parent, and likewise handleRatingUpdate updates all the data fields at once. We then pass back the data in App as props to both StarRating and RatingInputs, and each component updates its own data accordingly.

There are a couple of additional changes. First, all of the input elements have min and max attributes set, which will show an error highlight should any of the values go below the minimum or above the maximum. There’s also a limit property, which is used to constrain minRating and maxRating; this is mainly to control how many stars can actually be rendered, as rendering more than a thousand begins to negatively impact performance.

Better Validation #

We still haven’t solved the problem of preventing someone from inputing illegal values into our data. There are a variety of conditions we’ll need to check, and we’re going to need to check them for both StarRating and RatingInputs. It seems like it would be a good idea to write a separate file that contains all this validation code, which we can import into whatever files need to use it.

Conveniently, I’ve already written such a library for the React version of this project. Since we’re validating the exact same set of data, we can just take that file as-is and plop it straight into our Vue project.

In your src directory, create a new directory and name it lib, then create a file called validate.js and copy-paste the following code into it:

The function we’re most interested in is the default export, which is a function that runs all the other validation functions in the file, returning false if any one of the checks fail, and only returning true if all the checks pass. All we have to do is pass in our data values.

We have the validation library, but now we need to integrate it with our app. Let’s start with the RatingInputs component.

The only place where we needed to make changes was the script file. First, we import the library. Then, in the methods section of the component, we’ve added a new method: anyAreEmpty. All this does is check any arguments passed into it and return true if any of them are equal to an empty string. In our handleRating method, we then use anyAreEmpty to check each of our input values to make sure they aren’t empty strings. Why did we do this? A more detailed explanation can be found in this section of the React tutorial, but the gist is that without doing this, there are scenarios where Vue (and React) won’t let you update the inputs at all.

Finally, we’ve wrapped our call to $emit inside of our validation function, inputIsValid, which takes all of our data values as an argument. We’ve also moved our number conversion to occur before we run the validation function; otherwise, we’d be passing strings into the validation functions, which are expecting numeric values. Only when the validation function returns true do we emit the rating-update event.

With RatingInputs taken care of, let’s turn to StarRating and make a small change there (this is what the asterisk from earlier was for).

Here, we’ve added a new property to the component, called beforeMount. This is one of Vue’s lifecycle hooks, which are basically functions Vue calls at specific stages of a component’s life. In this case, beforeMount will be called once, right before the component is rendered for the first time. We’re checking to see if our default props, being passed in from App, are valid, and if they aren’t we throw an error to let the developer know that they need to check the values being passed as props.

You might be wondering: Why bother with this validation at all? After all, this validation check isn’t what’s showing the errors to the user; the browser is handling that. All this is doing is preventing the rating-update event from being emitted when an illegal value is entered. But that’s the point: without this validation, Vue would emit the event, try to process the illegal values, and then error out in the process. We’d be wasting processing effort doing something we already know is not going to be valid. While that’s not as significant an issue for a small demo app such as this, there are plenty of cases where this would be a costly error. For example, say our update event triggered an AJAX request; we wouldn’t want to send an HTTP request whenever an illegal value is entered. By making sure we only update when our input values are legal, we ensure our code isn’t wasting resources and processing power.

While I’m not going to do this here, we could also use our validation library to trigger more specific error messages in response to the illegal inputs. Feel free to add this yourself, if you’d like!

Final Touches #

With this, all that’s really left to do for RatingInputs is add some styling. If you want to fully match the way my version of the app looks, add the following style section to RatingInputs:

We should also change the styling on App so that the visual structure looks better (up until now we’ve been using the default styles that were generated by Vue when we created the app). I also made slight changes to the App template. To match my version of the app, add the following style section to App and modify the App template to match mine:

Of course, if you want to style things differently, feel free to do so!

Conclusion #

Hopefully this was a fun app to build. I liked making a demo app that wasn’t a todo list. Vue felt intuitive to me, as a web developer, and I loved being able to use template files to keep everything relating to a single component in one file. In exchange, you do have to structure your app in a more specific way, but since I mostly agree with how Vue wants you to do things, I don’t consider this a bad thing.

If you want to view the entirety of my code, look up the project on my Github repo.

Questions? Feedback? Leave a comment!

Sample Project: Star-Rating (React)

This is part one of a multi-part series about React and Vue.
Part 1: React
Part 2: Vue

Lately, I’ve been learning React, a JavaScript front-end framework. As of now, it is one of the most popular JS frameworks in existence. To help myself learn the framework, I built a sample project: a Star-Rating app. It functions similarly to what you might see on Amazon, where you have a rating represented as a line of stars. You can change the parameters used to calculate the star rating via an inputs component.

To see the final result, click this link.

I think it turned out rather nicely, and today I’d like to show how I built it, and in the process explain how React does things, as best as I currently understand it.

This tutorial presumes a basic knowledge of HTML, CSS, and JS, along with some basic knowledge of how to develop using a command line interface (or CLI) and how to use Node Package Manager (or NPM). If you’re unfamiliar with any of these things, feel free to make an online search for whatever you don’t know.

Table Of Contents

Setting Up
Looking Around
Creating Our First Component
Rendering Stars with FontAwesome
Calculating How Many Stars to Render
Rendering the Stars
Adding Our New Component
Introducing a Second Component
State and React
Passing State Between Components
Wiring Everything Else Up
Better Validation
Bug Hunt
Final Touches
Conclusion

Setting Up #

To start the process, I chose to use create-react-app to handle creating the base project structure. You can get it through NPM. Using the command line:

# Install create-react-app. You only need to do this if you don't already have it installed.
npm i -g create-react-app

# Create a new React single-page app.
create-react-app react-star-rating

# Navigate to the app directory and fire up the development server.
cd react-star-rating
npm start

After performing the above instructions, a tab should automatically open up in your default browser to localhost:3000, which should be a page containing a spinning React logo and some additional text.

It should be noted that you don’t need to use create-react-app to make a React project; instead, you could just include CDN links of these two scripts in an HTML file:

<script crossorigin src="https://unpkg.com/[email protected]/umd/react.production.min.js"></script>
<script crossorigin src="https://unpkg.com/[email protected]/umd/react-dom.production.min.js"></script>

I find create-react-app offers conveniences that are useful, such as hot reloading (changes are updated as you save your files) and a development server (so I don’t have to set one up myself), so I usually prefer to use that. This tutorial will assume that you are also using create-react-app.

Looking Around #

Now that we have the barebones project set up, let’s open the project directory in a code editor. You can use whatever editor you wish; I personally use the free editor Visual Studio Code, but there are plenty of other excellent options, such as Atom and (if you’re willing to spend money) Sublime.

Once you have the project open in the code editor, you should see there’s a bunch of files already set up. Let’s look in the src directory and open up the file called App.js. It should look something like this:

One thing you’ll notice right away is the use of import and export statements, as well as the use of classes. These are all part of the ES6 syntax. React makes heavy use of ES6 features; if you’re not familiar with ES6, here is a quick guide of basic features. Not all ES6 features are yet included in modern browsers (as of this writing), so one of the things create-react-app provides is a “transpiler”, which automatically converts ES6 syntax to its ES5 equivalent, so browsers know how to read the code. If you want to work with React, I’d recommend becoming familiar with ES6 features and how to use them; once you know how it works, ES6 lets you do really powerful things.

I’ll occassionally point out instances where I’m writing ES6 and compare it to the ES5 version so you can see how much simpler ES6 can make things look.

So, what is the code for App.js actually doing? Let’s do a brief examination.

These import statements are including external files into our file. We can even use imports to add specific parts of a file to our app, which is what { Component } is doing. Notice that we can import more than just JavaScript; we’re also including a logo image (logo.svg) and our app’s CSS file.

Here, we are using an ES6 feature — classes — to extend the Component class, which means we are getting access to all the functionality that a Component class has. If we were to use ES5 syntax, we wouldn’t be using class syntax at all. Instead, we’d be calling a React function called createClass, like so:

Both this snippet and the one previous do exactly the same thing: create a new React Component class.

So, what is the “render” function for? Primarily, it is responsible for rendering our component’s HTML. You might have noticed the HTML elements being returned by the render function:

Strictly speaking, this isn’t actually “HTML”. It’s something called “JSX”. The React site gives a good explanation of what JSX is, but to summarize, it’s a way to write HTML markup using JavaScript. This is one of React’s biggest features: the idea that components return their own HTML to render on the page. Some people really love the idea, as it allows you to keep the logic and markup of a component in the same file, which can make it easier to reason about an app’s logic. The JSX makes it easier to understand what the component’s markup looks like. React takes care of converting JSX to rendered HTML for you.

Some say that you shouldn’t mix JavaScript and HTML/JSX together. If you belong to this camp, then you don’t have to use JSX at all. Everything you can do in React using JSX, you can also do through pure JavaScript. The following code does exactly the same thing as the previous snippet, but just using JavaScript:

As you can see, under the hood JSX is just a series of JavaScript functions and objects, which end up creating our HTML elements. Personally, I find this syntax more difficult to read, and thus harder to reason with when I’m building an app, so I like to use JSX. That’s what I’ll be using for the remainder of this tutorial.

There’s another point of interest we should look at in the JSX code: the {logo} which is being set as the image tag’s source. In JSX, if you want to use JavaScript variables, you can do so by adding that variable directly to the JSX code, enclosing it in curly brackets. This is what will allow us to show various bits of data that we calculate with JavaScript.

Finally, at the end of the file, we have one final line:

As we use import to pull stuff in from other files, we use export to send stuff to other files that want to import our code. In this case, we are exporting the App component class.

Now that we’ve analyzed the App.js file provided by the boilerplate code created with create-react-app, and in the process familiarized ourselves a little bit with how React works, let’s start actually creating our app!

Creating Our First Component #

First, let’s make a folder called “components”, which will house all of our component files. With our components folder created, let’s add a new file to it: StarRating.jsx. Note the .jsx extension. It signifies that we are using JSX in this file. You could, technically, just make it a regular .js file (and, if you noticed, the file we looked at earlier is App.js), but I prefer to use the .jsx extension because it helps me know which components are rendering HTML.

Now that we have our first component’s file, let’s write some code.

This is just adding some code to get us started. We import React and the Component class, and we export a new class, StarRating. I’m defining the class as part of the export, instead of creating the class first and then exporting it, as we saw in App.js. Either way works; I prefer to use this method. We also create a render function and return a single <div>, with a class called star-rating. Note that, instead of class, we use className. Since JavaScript itself uses the word “class”, when writing JSX we need to distinguish between an HTML class and a JavaScript class, and this is done by using className when writing an element’s classes.

You may notice that I’m not using semicolons when writing my JavaScript. In many cases, the browser takes care of adding the semicolons you need, so you don’t actually need to write them yourself. This is just a personal preference I have; you can choose to write or not write semicolons, as per your own tastes.

The other thing we’ve added is some default props, which React requires you store in an object assigned to the static property defaultProps. What are props? They are the data that gets passed in by a parent component, and the child component can then take that data and use it as it sees fit. In a bit, we’re going to modify the App component to do just that, but until then, we’re going to use these default values. It’s also good practice to include default values, as it helps show what props your component is expecting.

Here is a quick explanation of what each prop is for:

  • minRating: This controls the lowerbound value of our rating. Defaults to 0.
  • maxRating: This controls the upperbound value of our rating. Defaults to 10.
  • rating: The actual value of our rating. Defaults to 5.
  • starRatio: This controls how many rating points it takes to render a full star. Defaults to 2, which means it takes a rating of 2 to render a single star. With a default rating of 5, this will result in rendering two and a half stars.
  • limit: We’ll use this to set a limit for how large the min/max rating values can be. This is purely to improve app performance.

Rendering Stars with FontAwesome #

Next, we need to add some code that will allow us to render stars. To do this, we’re going to install a font library called FontAwesome, which has thousands of custom icons stored as scalable SVGs. This way, we can style our stars however we want. Specifically, we will be using the react-fontawesome package, which integrates FontAwesome with React.

First, we’re going to install a few libraries via NPM and save them to our package.json:

npm i --save @fortawesome/fontawesome @fortawesome/fontawesome-free-solid @fortawesome/fontawesome-free-regular @fortawesome/react-fontawesome

To summarize, we’re installing Fontawesome, two sets of Fontawesome icons, and a package which provides FontAwesome React components. Once the packages have finished installing, we need to import them into our project.

First, we need to go back to App.js and add a few lines:

The imports work just like our previous import statements. The line fontawesome.library.add(solid, regular) is a function of fontawesome which will take the function arguments and, if they are valid fontawesome icons or sets, make them globally available to all our other components (so we don’t have to individually import the icons in each component file). In this case, we’re importing all of the “solid” and “regular” icons and making them globally available. We could also, if we wish, pull individual icons from each set (like we did with React’s Component) and add each one to our library. This results in slightly improved performance; however, since this is just a simple project example, I’m going to add the entire solid and regular sets of icons to the library.

Next, let’s go back to our StarRating component and add one more import:

Here, we are importing the FontAwesomeIcon component. We will be using it to render our star icons. (Confused? Just keep reading, for now; there will be examples that should hopefully make things clearer.)

Calculating How Many Stars To Render #

If you played around with the final result at the beginning of the tutorial, you may have noticed three different types of stars: “full” stars, “half” stars, and “empty” stars. This is how I’ve chosen to represent the star ratings for this project.

Before we can render any of that, though, we need to calculate four things:

  • What is the maximum number of stars we should render?
  • How many full stars should we render?
  • How many half stars should we render?
  • How many empty stars should we render?

You may be wondering why it’s necessary to calculate a “maxStars” value. Sure, we could simply calculate the full, half, and empty star values without calculating a separate max value, but I’ve found that it makes the math simpler to just figure out the maximum number ahead of time and use it in calculations later, so that is what I’ve chosen to do.

The following is the StarRating component, with the addition of the code which will calculate the four values we want:

To briefly explain each calculation:

  • maxStars(): We take the maxRating and divide it by the starRatio, then round the answer up to the next integer.
  • fullStars(): We take the rating and divide it by the starRatio, then round the answer down to the next integer.
  • halfStars(): We take the modulus (or remainder) of rating and starRatio, get half the value of starRatio, and compare the two using a ternary operator. If `x` is greater than or equal to `i`, we return one half-star; otherwise, we return no half-stars.
  • emptyStars(): We take maxStars and subtract from it fullStars and halfStars.

ES6: When you are declaring methods inside of an object or class, you can use the shorthand notation yourFunction () {} instead of explicitly specifying yourFunction: function () {}. Also, I’m assigning the values of the props to variables within the calculation functions using ES6 destructuring, which is a convenient way to get values off of objects; in ES5, you’d have to use objectName.dataValue.

Rendering the Stars #

Now that we can calculate how many of each type of star we need to render, let’s get to actually rendering them! First, let’s render the fullStars. In our render function:

The first thing we do is assign fullStars to equal the result of our fullStars() function. Next, we create a new function within the render method, called renderFullStars(). Since we’re defining this within a class method, and not the class itself, we can’t use the shorthand notation from before; we have to explicitly assign the function.

ES6: I’m using an arrow function, but I could also have simply declared function renderFullStars() {}. Either way works; I prefer the arrow function syntax here because it looks cleaner to me.

The code in renderFullStars() may seem confusing, at first glance, especially if you’re not familiar with the various methods associated with arrays. However, once you know how it works, I think it’s actually easier to read.

First, we check to see if the number of full stars is 0. If it isn’t, then we proceed to create an empty array with the length set to the number of full stars. Using chaining, we then call fill() on the resulting array, which will fill the entire array with whatever value we want; in this case, null. Finally, we use the map() method to create an entirely new array, with exactly the same number of items, but instead of being null these items equal the result of the callback function we pass in (note I’m again using an arrow function). This final array is what gets returned. What if there are no full stars? In that case, we return an empty string; you’ll see why in a moment.

Let’s take a brief moment to examine the callback we’re passing into the map method:

Remember the FontAwesomeIcon component we imported earlier? We are now using that component as the return for our callback. We’ve given it a class of “star” (via className), which our CSS can target later. The next two things we are setting are props which we are passing to the FontAwesomeIcon component. I’ll go over each one.

The first prop we’re setting is key. This is something React uses to help keep track of lists of items, and while it is technically possible to exclude the key property from a list, it is strongly discouraged. The key should be a unique value, and to accomplish this I’m using the string “fs” plus the i argument, which is equal to the index number of the current array item. Note that, since we need to use a JavaScript expression to set the key value, we enclose it in curly brackets.

ES6: I’m using a template literal to set the value of the string, which uses the back-tick character ` instead of regular quotation marks. The ES5 equivalent is "fs" + i.

The second prop is icon. This tells the FontAwesomeIcon which icon we want to use. For full stars, we want to render star from the solid library.

At the end, we self-close the component. Generally, you can treat React components like self-closing tags, but you can also use <YourComponent></YourComponent> instead. There are cases where we’d want to use the opening/closing tag syntax, but this isn’t one of them. Generally, I prefer to use the self-closing tag format because it’s simpler to read.

Finally, in the return…

… we’ve added {renderFullStars()} inside of the <div>. Since curly brackets enclose JavaScript expressions in JSX, that means we’re running the renderFullStars function and outputting the return directly into the JSX. Take a moment and go look at the final result again. The default rating here is set to 5, which means two full stars and a half star are rendered. renderFullStars() creates two FontAwesomeIcon components and outputs them into the star-rating container, which is then rendered into HTML by React. What if there aren’t supposed to be any full stars? Then this function returns an empty string, and nothing is rendered.

Let’s go ahead and add the code to render half stars and empty stars:

As you can see, the process for rendering half stars and empty stars is the same as for full stars: pull in the calculated values, create render functions to render the components, and run the render functions in JavaScript expressions in the JSX. The only differences lie in the FontAwesomeIcon components we are returning.

For empty stars, there are two differences:

  • We’ve changed the key to use “es” instead of “fs”. This is to keep the key unique from the keys we generated for the full star components.
  • Instead of passing in a string for the icon, we’re instead passing in a JavaScript expression. This is because, by default, the FontAwesomeIcon assumes we want an icon from the solid library. Our star icon for full stars was part of the solid library, so we could just pass in the name of the icon. But for empty stars, we want to use the star icon from the regular library, so, as per the react-fontawesome documentation, we need to pass in an array. The first item in the array is the library we want to use, “far” (“FontAwesome Regular”); the second item is the name of the icon itself, “star”.

For half stars, we also use a different key prefix, “es”. But, instead of returning a single FontAwesomeIcon component, we’re returning two different icons, wrapped in a span. The reason for this is because of the way FontAwesome renders the half-star icons. Instead of rendering a single icon of a star half-full and half-empty, FontAwesome has a full half-star and an empty half-star.

To get the half-filled star effect we’re looking for, we need to take advantage of FontAwesome’s power transforms — specifically, Layering. What Layering lets you do is take two icons and combine them together so they appear as a single icon. You do this simply by adding the fa-layers class to an element containing the icons you want to combine.

So we’ll use the two half-star icons — star-half from “solid” and {['far', 'star-half']} from “regular”. We’ll also pass in flip="horizontal" to the regular half-star so it’s reversed. The end result is something looks like a half-filled star. Excellent!

At this point, we’ve finished* the StarRating component! Here’s what the code looks like:

Our component takes in a bunch of props, uses the values of those props to calculate how many star, empty star, and half star icons to render, and then returns the rendered stars in JSX. It’s time to go back to the App component and add our brand-new component!

Did you notice the asterisk on “finished”? There’s one other thing we’re going to address later, but we don’t need to worry about it right now.

Adding Our New Component #

To begin, let’s import the StarRating component and add it to our App component’s render return:

If you don’t have the local development server running yet, do so now by entering npm start into your CLI; if you do have it running, then upon save the page should automatically reload. At this point, you should see a row of black stars appear beneath the default text — two full, one half, and two empty. If you see this, congratulations, you’ve just rendered a React component! If you don’t see this, or if you get an error, try to go back over your code to make sure it matches what I’ve previously wrote; hopefully you’ll find what the problem is and fix it.

Assuming things are working, let’s go ahead and pass a prop down to StarRating:

Upon saving the file, you should see the rendered stars change. Assuming you passed in “7” like I did, you should now see three and a half stars. This is the power of props: a parent component passes the props down to the child, which then takes the props and automatically updates any data that is calculated using those props. In our case, we passed down a rating of 7, and StarRating converted that rating into full-stars, half-stars, and empty-stars and updated the rendered result automatically.

Of course, right now we can only update the rating by manually changing the value of the rating prop being passed to StarRating. That’s not very practical at all. What we want to do is have a way for us to input the values we want and have StarRating update accordingly, as seen in the final result. To do that, we’re going to need another component specifically dedicated to do just that…a RatingInputs component!

Introducing a Second Component #

Let’s start by creating a new component file in our components folder, calling it RatingInputs.jsx. Add the following code:

Just like with StarRating, we start with importing React and the Component class from the “react” package, then export a class, this time called RatingInputs. The class includes a render function which returns some JSX, as well as the same set of default props we used for StarRating.

I took the liberty of going ahead and including an input tag (and corresponding label) for our rating. Notice the use of htmlFor on the label instead of the usual for; like class, for is a reserved word in JavaScript, so JSX provides an alternative for the HTML attribute.

In case you’re wondering why you aren’t seeing this show up on the page, remember that RatingInputs hasn’t yet been imported into the main App component. You could do so now, if you wish. You just might get some errors flashed to the page as you make changes to the code.

As of right now, the rating input isn’t wired up to anything; it’s just a dumb number input element. To remedy that, let’s begin by pulling the rating prop into our render function and setting the input’s value equal to it:

Notice that I’ve added an attribute called “ref” to the input. What’s that for? This is how we’re going to tell React what element our rating input is. I’ve also added an attribute called onChange; this is, in fact, an event handler. The way React/JSX handle event handlers is different from using your typical addEventListener(), and you can read more about it here. The value of onChange is set to a JavaScript expression: this.handleRating. That will call a method on our RatingInputs class called handleRating.

Wait, we don’t have a method called handleRating? Let’s fix that:

Currently, all our handleRating function does is save the value of this.refs.rating.value as a number, but we’ll change that soon. The other change is the addition of a constructor function to our class. In other programming languages, constructor functions are run every time a class is constructed, and here is no different. We call super() to call the parent class’ constructor (Component), and after that we bind the this value of our handleRating function to the this value of our class.

Why are we doing this? Well, when we run a callback function in our JSX, the this value of the callback function will be undefined. Thus, we need to explicitly bind the value of this in the callback function to the this of our class so we can access this.refs.

Now we have a callback function that will run every time we update the value of the rating input. How are we going to send that value from the RatingInputs component to the StarRating component? By passing it up to the parent App component and having it pass the value down to StarRating.

State and React #

Before we implement the code that will transfer our rating data from RatingInputs to StarRating, we need to discuss the concept of state in React. A fundamental concept powering React is the idea that no child component should ever modify any prop passed down to it. Doing so would mean the parent component’s state could be modified in a way the parent doesn’t control. When that happens, you can’t always be sure what is modifying the state.

Instead, it is the responsibility of the parent component to provide the child component with the means to request changes to the parent’s state. That way, you know the only way the parent’s state can be modified is through methods the parent itself provides. The parent then passes the updated state back down to the child component, as well as to any other components receiving props from the parent.

With that in mind, we can have App pass one of its own methods down to the RatingInputs component that, when run, will update the state in App. Since StarRating also receives that same state from App, it will receive the changed data from App as soon as it is updated. Thus, we preserve the sanctity of the state by not allowing RatingInputs to modify it directly; instead, RatingInputs asks App to update its state by calling the specified update request function, and App handles changing its state and passing the updated state down to both RatingInputs and StarRating.

Enough theory, let’s code.

Passing State Between Components #

First, in RatingInputs, we’ll add the following to handleRating():

This will call a function passed to RatingInputs as a prop, called onStarRatingsUpdate.

Next, let’s update App:

Whoa, there’s been quite a lot of code added! Let’s break it down, piece by piece.

First, we added an import statement for our RatingInputs component. The next addition is a constructor for our App class:

Once again, we call super() to run the Component class’ constructor, and we also bind the this value for a function called this.handleStarRatingsUpdate, similarly to what we did for handleRating(). Finally, we set a property called state to be an object, with one member, rating, set to 5. This state property is how components handle state within themselves. We will be passing this state down to both StarRating and RatingInputs via props.

Confused about the difference between “props” and “state”? Props are data that flow from a parent component to a child; state is the data within a component.

Next, we add a function called handleStarRatingsUpdate:

In React, outside of the constructor, the only way state should be modified is by calling a component’s specific setState function, and this is what we’re doing here. We’re passing in an object to setState, and setting the properties of the object using ES6’s spread syntax. Basically, the spread operator will take all the properties of one object and copy them over to a different object. In this case, we’re first taking all of the values in this.state (the App component’s state), and then we’re taking all the values from the data argument and adding them to the object being passed into setState. This results in any properties from data overwriting any similarly-named properties pulled in from this.state.

Still confused? Keep reading; there will be a demonstration of what this accomplishes, and hopefully that should clear things up.

Spread syntax also works with arrays! Read the reference link to learn more.

Lastly, we update our render function:

We’re pulling in rating from this.state. We’ve also replaced the hard-coded prop being passed into StarRating with this rating value. Finally, we’ve added our RatingInputs component below StarRating, and we’re passing two props to it: rating and onStarRatingsUpdate, which we’ve set equal to our function handleStarRatingsUpdate. Why the two different names? It’s just a convention: we say “on” to refer to the prop, and “handle” to refer to the actual function which handles the call from the child component. We could easily have both the prop and the function be the same, if we wanted. My preference is to use the on/handle convention.

Assuming nothing went wrong, once you save App.js you should see the rating label and input appear immediately below the black stars from StarRating. Try modifying the value of the input. If everything was done correctly, as you change the value of the input, the rendered stars should also update right along with it. Our components are talking to each other through the parent!

If you were confused by the contents of handleStarRatingsUpdate(), perhaps now it’ll make more sense. When we update the rating input, it calls handleRating(), which takes the value of our rating and passes it as an argument into onStarRatingsUpdate(). This runs handleStarRatingUpdate(), on App. It calls setState(), which first sets state equal to this.state (in other words, the previous state values). Next, it adds any values passed in via data; any state properties with the same key as the properties passed in by data get overwritten with the value from data. With the state updated, App passes the state back down to both RatingInput, where the value of the rating input is updated, and StarRating, where the updated rating is calculated as stars and rerendered onto the page.

As you play with the rating input, you may find you’ve triggered an error called “invalid array length”. This is because we’ve set rating to a value lower than minRating or higher than maxRating, and our star calculations can’t handle that. To prevent us from setting the rating too low or too high, let’s start by setting the min and max values on our rating input:

Now, when we toggle the rating input up and down, we should be stopped when our rating gets down to 0 or up to 10. That’s good enough, for now; we’ll be implementing a better fix shortly.

Wiring Everything Else Up #

Now that we have our rating wired up, let’s go ahead and wire up minRating, maxRating, and starRatio. When you’re done, your App and RatingInputs components should look something like this:

We’re setting the min values of both minRating and maxRating to 0, and the max values to limit. You technically don’t need to include a limit, but in testing I’ve found that adding min/max ratings resulting in over 1,000 rendered stars introduce the possibility of performance issues. Thus, I made the decision to limit how high those values can be increased.

Now all of the values are dynamic: we can set the maxRating to 20, our starRatio to 1, our minRating to 5…any number of combinations, really! As you experiment with the possible combinations, however, you might notice something: if you directly set the input values to something illegal (instead of using the increment/decrement buttons), you’ll trigger the “invalid array length” error. While setting the min/max attributes of rating stops the input increment/decrement buttons from going out of range, they do not prevent us from entering such an illegal value directly. Also, you could change minRating to be higher than maxRating (or maxRating lower than minRating), and trigger an error that way.

We clearly need better validation than what we can get from HTML. Let’s implement it!

Better Validation #

First, we’ll create a new folder in src and call it lib. We’re going to make a library of functions we can use to validate our inputs. Make a new file in lib and call it validate.js, then place the following code into it:

With ES6 exports, you are not limited to only exporting one function; you can export as many as you want. You just have to refer to them explicitly by name when you pull them out. For example, if you wanted to use the minLessThanMax function, you’d write the import statement as import { minLessThanMax } from 'path/to/validate'. You can specify a single function as the default export; this is what will be returned if you simply do import ThisFunction from 'path/to/validate'. In our case, all we really need is the default function in validate. I just wanted to illustrate what export is capable of, and that you aren’t limited to one export per file.

Let’s look at the default function we’re exporting. We’re taking in arguments for rating, minRating, maxRating, starRatio, and limit, and then passing these arguments into various validating functions. The key is that we’ve strung the return as a chain of functions using the && (and) operator. The moment any one of these validating functions returns false, the entire function returns false; otherwise, if all the validating functions return true, the function returns true.

For a couple of the validating functions, I use a combination of rest parameters in the function argument and the filter array method to validate one or more possible values with just a single line of code. Read up on the two links to figure out how this works!

Now that we have our validation library, let’s start by importing it into RatingInputs:

As you can see, all we needed to do is import the default validate function as inputIsValid, and then wrap our call to onStarRatingsUpdate() in an if statement which checks to see if inputIsValid() returns true. If it doesn’t, then nothing happens. The inputs won’t update, which means you can’t change them to anything which would cause validation to fail.

We also need to import our validation library into StarRating (this is what the asterisk was for):

Here, we are once again importing the validate library as inputIsValid. This time, we’re using a new function called componentWillMount to run the inputIsValid check, and if it fails we throw an error.

What is this for? componentWillMount() is a part of how React components work, one of the so-called “lifecycle methods”. A lifecycle method is just a function that a React component runs at a specific moment, such as before props are passed down, or before/after a component is rendered. In this case, we are using the componentWillMount lifecycle method, which runs right before a component is first rendered. Because we are checking inputIsValid() at this point in time, we can make sure the initial props passed down by App are valid, and if they aren’t we’ll throw an error telling the developer that they need to make sure rating is between minRating and maxRating (as this is what’s causing validation to fail).

If you want to learn more about lifecycle methods, read this.

Bug Hunt #

Now, things seem like they work. If you try to directly input a minRating of 11 when the maxRating is 10, you’ll find yourself unable to do so. Same with if you try to make rating exceed the bounds of the min and max ratings. Yet there is still a bug, which I didn’t notice until I started messing around with the project one final time before submitting this post.

What is the bug? Click on the rating input, then try to backspace. Normally, when you do this, you’d expect the input field to be empty; however, our app will either refuse to delete the value at all, or it will set the value to 0. Peculiar, indeed, and not very user-friendly.

Why is this happening? Well, in our handleChange function in RatingInputs, we automatically convert each ref’s value to a number when we assign it. This is because refs are stored as strings, so we want to make sure we’re working with numbers when we’re performing validation. What happens when an empty string gets converted to a number? It becomes 0. Thus, when we assign the value of an input that was cleared by the user — an empty string — we’re assigning the variable a value of 0. When minRating is 0, this results in rating being set to 0; otherwise, 0 is lower than minRating, so our code doesn’t allow the update to be made.

How do we fix this? We’ll need to refactor the RatingInputs component to have its own state, instead of merely rendering the props App passes down to it. This way, we clear the input fields, but delay sending an update until the inputs have values that are validated. Why wasn’t this done in the first place? When I first designed the component, I didn’t think it needed to have its own state, so I simply wired everything up through props. Now, however, it’s clear that we need the inputs to be able to have values separate from the parent state, so giving RatingInputs its own state is appropriate.

Here is what the changes will look like:

First, in the constructor, we set the initial state of the component to be equal to the props being passed down to it (we also accept props as an argument). Next, we add a new function, anyAreEmpty, which just checks to see if any of the arguments passed into it are empty strings.

We make quite a few changes in handleRating():

  • We change our assignments to get the raw ref values, instead of converting them to numbers.
  • We update the state of our application to match the value of the refs.
  • We use the anyAreEmpty function to see if any of the variables are empty strings, and if so we return.
  • Having verified we have no empty strings, we convert the ref values to numbers.
  • Finally, as before, we pass the numeric values through our validation library, and only update when validation passes.

Lastly, in our render function, we get our default values from the state of RatingInputs, not the props passed down from App.

When these changes are implemented, you’ll notice that you can now set the input fields to illegal values once again. However, no updates are sent to App, so StarRating is not updated. Additionally, because of the min/max attributes set on the rating input, an error highlight appears around the element when an illegal value is set. I consider this “good enough” error notification for a sample project.

Final Touches #

At this point, our Star-Rating app is fully functional and secure against bad data. The only thing left to do (if you want) is to restructure the app and change the styling to make it look like the final result. We’ve already done a lot of hard, good work, so I’ll simply provide you with the JSX you’ll need for App.js and the styles you need to replace within the App.css file to make it match my version of the app:

Of course, if you want to make your Star-Rating app look completely different, then feel free to do your own thing!

Conclusion #

I hope you had fun building this app; I certainly enjoyed doing something different from a Todo list as a sample project. React, once you get used to some of the ways it wants to do things, is a fun front-end framework to build apps with. Even though it may seem like it takes some effort to do things with React, the way it is designed helps you think logically about the structure of your code, helping you avoid writing smelly code and making it (mostly) clear how your app works.

If you want to view the entirety of my source code, look up the project on my Github repo.

Questions? Feedback? Leave a comment!

Star-Rating Part 2: Vue