Terry Beard

Nov 19, 2024 • 6 min read

A Super Simple Signal in Angular

May not be the "right" way, but it works

As part of my development portfolio for work, I created a forms engine - formGen - in Angular. The engine leverages a JSON file that contains a definition for a form and renders it to the page via a specified route. When a value is changed in the form (specifically after the blur() event), it is saved to the database.

The formGen engine supports various UI types such as inputs, dropdowns, checkboxes. It also allows for conditional questions so that if the "parent" question has a particular value, children questions could be displayed. This capability gives us a lot of flexibility to our functional areas with regards to the types of questions and responses that can be contained in the form.

But one missing piece was the ability to base a list of responses from a prior question. The form loads all of it's data at one time so any and all possible responses are baked into the form. So let's say that a student wants to declare their major. They would start by filling out the Major Declaration form. One of their first steps is to select their major they want to pursue such as COMP. Then the student might need to select a faculty member to be the advisor for that major. In it's current form, the form generator would have to preload all possible faculty into that list. And even with parent / child questions, there was no good way to filter that list down to just the Computer Science faculty.

My first attempt to resolve this issue was to create an EventEmitter() that would fire off if a form response was changed. The problem with this is that formGen functions recursively - you can have as many parent / child / grandchild... questions as processing power allows. This would require an emitter to bubble up across all those question components to the form component to force a data refresh. While I'm sure this could be done, I couldn't quite figure it out. And not being able to figure something out is my first clue to maybe I shouldn't be using that particular method.

My next thought was to use an Angular Signal. My initial understanding of a signal was that it was an application wide thing that could be used to track changes or carry values. And reviewing tutorials and working with a signal for a few hours, it seemed like a perfect fit for the application.

There are plenty of good tutorials out there covering signals, but I wanted to share my experience and hopefully save you some time if you should leverage them in your application.

My first step was to decide how I was going let the form know on what question to run the update of the data. This was probably the easiest step. I basically just added a value to the JSON structure of the form definition.

//Sample form form definition

{ "questions": [
    {   "questionId" : "CUR_APPLICATION_NAME",
        "ui" : "input",
        "label" : "CLIQ menu name of application / page",
        "refreshOnSave" : true, --added to indicate refresh
        "tab" : ""
    },	
    {    "ui": "componentText",
         "cfcomponent": "mtfuji.forms.formService",
         "questionId": "RETURNED_CUR_APPLICATION_NAME",
         "cfmethod": "getMenuNameEntered"
     }
}

The formGen engine loads each question in the form and carries over all the necessary properties in each question's descriptions from the form definition. This makes updating forms really easy. But when adding new functionality, this generally requires and update to the Angular application. In this case the refreshOnSave property was added and the engine expects a boolean value. If the refreshOnSave is true then when the question data is saved, the form data will need to refresh.

The next step was to tell the form engine to actually do the refresh. This is where a signal enters the solution. First I had to tell the application that I wanted to use a signal and export that signal as it's own constant. This is done in my main component called form.

import { signal, effect } from "@angular/core";

export const questionUpdate = signal(false);

By default, the questionUpdate signal will be set to false because we only want to update the form data on the page when a question requires it.

Next, the save process needed to check to see if the refreshOnSave was set and then update the signal accordingly. But first we have to let the question component know that we want to use the signal. So we need to import it and then declare it:

import { questionUpdate } from './app.form';
export class QuestionComponent implements AfterViewInit{

    public questionUpdate = questionUpdate;

    ...
}

The save functionality is code in the question component.

saveData(person: any, question: any, answer: any, definition: any, formId: any){
    this.formDataService.saveData(person, question, answer, definition, formId).subscribe(
        value => {} ,
        e => console.log(e),
        () => {
                   if (question.refreshOnSave) {
                    this.questionUpdate.set(true);
                }
            }
        );
    }

The key code is the addition of:

 if (question.refreshOnSave) {
     this.questionUpdate.set(true);
 }

( Technically, I could have just coded: this.questionUpdate.set(question.refreshOnSave) but I figured I might eventually want to do more in the if block. )

And now the final step. We need to tell the form component that the signal is now set to true and therefore the form data needs to be refreshed. This is done with the effect API. For my application, I put this code into the constructor of the form component. I don't know if it has to go there, but that's where the tutorials were leading me.

    constructor(
        private formDataService : FormDataService,
        private route : ActivatedRoute,
        private router: Router,
        private snackBar: MatSnackBar

        
    ){
        effect(() => {
                console.log("I've been effected.");
                //console.log("questionUpdate" , questionUpdate);
                if (questionUpdate()) {
                    console.log('question updated, refreshing form data')
                    this.formDataService.getForm(this.formName).subscribe((response: any) => {
                        this.formDefinition = response;
                    })
                }
                questionUpdate.set(false);
            },
            {   
                allowSignalWrites: true

            }
        )

    };

If my understanding is correct, the effect API fires every time a signal is .set or .update. And it is here where we test our signal values and what we want to do with those results. In this case, the questionUpdate() constant is checked for it's value. If that value is true (set from the question), then the form's data service will refresh the all responses to the form. And we also set the signal back to false because the next question on the form may not require a refresh.

Also, in order to update a signal within the effect you must include the allowSignWrites: true. I believe this may be necessary to prevent circular effect updates.

So this is pretty much I know about signals. Definitely a win for the Angular environment. I can see though they could be a maintenance nightmare over the long term so I would recommend using them sparingly.

Hopefully, you found this helpful. Please feel free to share any feedback because that's how we grow as designers and developers.

Join Terry on Peerlist!

Join amazing folks like Terry and thousands of other people in tech.

Create Profile

Join with Terry’s personal invite link.

1

7

0