Travis Toulson
Administrator
Administrator

State Pattern in Service Portal.png

Check out other articles in the C3 Series

 

Complex multi-step forms present unique challenges in web application development: managing user progress, handling validation across steps, coordinating asynchronous operations, and providing graceful error recovery. When we built the photo intake process for CreatorCon C3, we needed a solution that could handle camera access, AI processing workflows, and dynamic user interactions while also keeping it maintainable under the chaotic development cycles.

 

To solve the problem, I leaned into a modified version of the State Pattern tailored to better support Service Portal and the untyped JavaScript language. This resulted in creating a flexible, maintainable approach that handled our complex requirements while remaining easy to modify and extend.

 

The Challenge: Multi-Step Photo Submission Form

 

Intake State Transition DiagramIntake State Transition Diagram

 

Our photo intake process required managing several complex workflows:

 

Take Photo Page Flow:

 

  1. Welcome → Next button advances
  2. Describe Yourself → Previous/Next navigation
  3. Take a Photo → Camera access and photo capture
  4. Confirm Photo → Retake or submit options

 

Create Your Card Page Flow:

 

  1. Complete Your Profile → Form validation and server updates
  2. Select Your Hero → Choose from AI-generated avatars
  3. Confirm Avatar → Final selection with fallback options
  4. Next Steps → Feedback collection and completion

 

Each step involved different types of interactions: form validation, camera API access, server communication for AI processing, and error handling for various failure modes. Traditional approaches like simple step counters or linear wizard patterns couldn't elegantly handle the dynamic nature of our requirements.

 

Why the State Pattern Made Sense

 

We considered several approaches for managing this complexity:

 

Alternative Approaches We Evaluated:

 

  • Catalog Item: Expand / collapse sections just aren't as satisfying a user experience as multi-step forms and would require custom widget work anyway for things like the camera.
  • Multiple separate widgets: Would require complex cross-widget communication and would make editing / refactoring more complicated
  • Multiple separate pages: Harder to maintain smooth user experience

 

State Pattern Advantages:

 

  1. Clear state encapsulation: Each step's logic contained in discrete, manageable chunks
  2. Easy modification: Adding or removing states just required object additions/deletions
  3. Modular design: States could be easily moved between widgets if needed
  4. Error handling: Errors became states themselves, providing consistent UX
  5. Centralized logic: All workflow logic in one place for rapid iteration

 

The rapid development timeline was crucial. Changes to the app were coming quickly and having centralized logic made refactoring much more efficient than jumping between multiple widgets or pages.

 

Our Modified State Pattern Implementation

 

Rather than using classical inheritance and handle functions, we leveraged JavaScript's flexibility to create a state object where each state contains discrete functions for each possible transition.

 

Core State Object Structure (Widget Client Controller)

 

// Enum listing the options for what stage of the UI flow
// is visible.
c.Stage ={
	START: {
		name: 'START',
		next: () => {
			if (!c.data.isSubmissionEnabled) {
				goToStage(c.Stage.DISABLED);
				return;
			}
			
			if (!c.data.isUserOnWhitelist) {
				goToStage(c.Stage.NOT_ON_WHITELIST);
				return;
			}
			
			if (!c.data.canRetry) {
				goToCreateYourCard();
				return;
			}
			
			if (c.data.hasSelectedImage) {
				goToCreateYourCard();
				return;
			}
			
			goToStage(c.Stage.PERSONAL_DATA);
		},
	},
	PERSONAL_DATA: {
		name: 'PERSONAL_DATA',
		previous: () => goToStage(c.Stage.START),
		next: () => goToStage(c.Stage.TAKE_PHOTO),
	},
	TAKE_PHOTO: {
		name: 'TAKE_PHOTO',
		previous: () => goToStage(c.Stage.PERSONAL_DATA),
		next: function takePhoto() {
			c.photoRequested = Date.now();
			goToStage(c.Stage.REVIEW_PHOTO);
		},
	},
	REVIEW_PHOTO: {
		name: 'REVIEW_PHOTO',
		previous: () => {
			goToStage(c.Stage.TAKE_PHOTO);
			c.photo = undefined;
		},
		next: submitPhoto
	},
	CREATING_AI_IMAGES: 'CREATING_AI_IMAGES',
	DISABLED: 'DISABLED',
	NOT_ON_WHITELIST: 'NOT_ON_WHITELIST',
	CAMERA_ERROR: 'CAMERA_ERROR',
};

 

 State Management Functions (Widget Client Controller)

 

The state management itself is elegantly simple:

 

// Controls which step of the form is visible 
// Initialize to the first stage
c.currentStage = c.Stage.SELECT_IMAGE; 

// Navigate UI to a particular step in the form stages
function goToStage(stage) {
	document.activeElement.blur();  // UX: Remove focus from current element
	c.currentStage = stage;
}

 

Form Page Managed by State (Widget HTML Template)

The HTML templates use AngularJS directives to show/hide content based on current state as well as exposes an actions container that uses the current state's functions to transition to other UI states.

 

<div class="cctcgStage" ng-show="c.currentStage == c.Stage.START">
    <h1 class="displayText"><span class="displayTextAccent">C3:</span> The Collectible Community Card Experience!</h1>
    <div class="bodytext">
      <p>
        Get ready to bring your hero persona to life! ⚡
      </p>
      <p>
        Submit your picture, answer some fun questions, and watch as our AI transforms you into a comic book-inspired legend!
      </p>
      <p>
        If you’re attending a CreatorCon event, you’ll build your own avatar, connect with other attendees, collect cards, and earn points for your team, all by showing up and scanning your way through the day.
      </p>
      
      <p>
        Get ready to collect, connect, and conquer. Start building your avatar now!
      </p>
    </div>
    
    
    
    <div class="cctcgActionContainer">
      <button class="cctcgButtonPrimary" ng-click="c.currentStage.next()">
        Next
      </button>
    </div>
</div>

 

The key insight is in the button's ng-click directive: c.currentStage.next(). This directly calls the transition function defined in the current state, making the HTML declarative while keeping the transition logic encapsulated in the state object.

 

Handling Asynchronous Operations and Errors

 

Server Logic Affecting UI State

 

Many state transitions require server communication for AI processing, data validation, or persistence. Our pattern handles this seamlessly as the next state example from our Create a Card page shows. We could easily modify it to use the response object or an error capture to redirect to other states as needed.

 

'COMPLETE_PROFILE': {
	name: 'COMPLETE_PROFILE',
	next: () => {
		c.server.get({
			action: 'UPDATE_PROFILE',
			fields: c.data.profileFields,
		}).then((resp) => {
			goToStage(c.Stage.SELECT_IMAGE);
		});
	}
}

 

Error States as First-Class Citizens

 

Rather than treating errors as exceptions, we model them as states in the workflow. In most cases, error states were dead ends with no transitions, so they were represented as strings instead of objects:

 

REVIEW_PHOTO: {
	name: 'REVIEW_PHOTO',
	previous: () => {
		goToStage(c.Stage.TAKE_PHOTO);
		c.photo = undefined;
	},
	next: submitPhoto
},
CREATING_AI_IMAGES: 'CREATING_AI_IMAGES',
DISABLED: 'DISABLED',
NOT_ON_WHITELIST: 'NOT_ON_WHITELIST',
CAMERA_ERROR: 'CAMERA_ERROR',

 

This approach provides several benefits:

 

  • Consistent UX: Errors are presented the same way as normal states
  • Clear recovery paths: Each error state can define how users can recover
  • Easy debugging: Error states are visible in the state object
  • Graceful degradation: Alternative paths can be provided for different error types

 

Cross-Widget Data Coordination

 

Our implementation uses server-side data to coordinate between the "Take Photo" and "Create Your Card" widgets:

 

  1. Profile Record: Central data store linked to user's sys_id
  2. Photo Submission Record: Stores photo data and processing state linked to a profile
  3. Generated Images Records: AI-generated avatars linked to a photo submission

 

Each widget queries the server on initialization to load the user's current data, making the widgets effectively stateless at the UI level while maintaining state server-side.

 

Implementation Benefits and Trade-offs

 

Benefits Realized:

 

1. Rapid Development Agility
During our three-month development cycle, requirements changed frequently. The state object structure made modifications simple - adding a new validation step just required inserting a new state object.

 

2. Clear Debugging and Testing
State-driven behavior made debugging straightforward. We could easily see what state the user was in and what transitions were available. Console logging state transitions provided clear audit trails.

 

3. User Experience Consistency
Every interaction followed the same pattern: current state determines available actions, transitions handle side effects, new state determines new UI. This consistency improved both development velocity and user experience.

 

4. Error Handling Integration
Modeling errors as states rather than exceptions provided natural error recovery flows and prevented the application from getting into undefined states.

 

Trade-offs and Limitations:

 

1. Big Big Widget Performance
With complex state objects, the size of the page and widget grows with every state. In our case, performance remained acceptable, but very complex state machines might require optimization.

 

2. State Persistence Complexity
While our server-side approach worked well, implementing true client-side state persistence (via URL parameters or localStorage) would require more sophisticated state serialization.

 

3. Testing Complexity
Unit testing state transitions required careful setup of mock server responses and state dependencies. Integration testing became more important than traditional unit testing. A separate page per step would likely have been much easier to test.

 

Lessons Learned and Best Practices

 

What Worked Well:

 

1. Keep State Logic Pure
State transition functions should focus on business logic, not UI manipulation. The goToStage() function handles all UI concerns.

 

2. Use Descriptive State Names
COMPLETE_PROFILE and VERIFY_IMAGE are much clearer than STEP_1 and STEP_2. This improves both debugging and code maintenance.

 

3. Design Error States Early
Don't treat error handling as an afterthought. Design error states and recovery paths as part of your initial state design.

 

4. Leverage Framework Features
AngularJS form validation (profileForm.$valid) integrated naturally with our state transitions, reducing custom validation code.

 

What We'd Do Differently:

 

1. State Persistence Strategy
I would love for the browser back and refresh to load back to the user's previous state. For that we'd have implement URL-based state persistence to direct link to specific steps.

 

2. State Machine Visualization
A visual representation of states and transitions up front would have helped during development and documentation.

 

3. More Granular Error States
Some of our error handling was still too broad. More specific error states would have provided better user guidance.

 

Implementation Guidelines

 

When to Use This Pattern:

 

  • Multi-step forms with complex validation requirements
  • Asynchronous workflows requiring user interaction at multiple points
  • Error-prone processes needing graceful recovery mechanisms
  • Rapid development environments where requirements change frequently

 

When to Consider Alternatives:

 

  • Simple linear workflows where a step counter would suffice
  • Purely server-side processes where Flow Designer is more appropriate

 

Implementation Checklist:

 

  1. Map out all states and transitions before coding
  2. Design error states as part of initial planning
  3. Keep transition functions pure and focused on business logic
  4. Use descriptive naming for states and transitions
  5. Plan for state persistence if needed for user experience
  6. Implement logging for debugging state transitions
  7. Test state transitions comprehensively, including error paths

 

Conclusion

 

The State Pattern, adapted for modern JavaScript and ServiceNow Service Portal development, provided an elegant solution for managing the complex multi-step workflows in our C3 photo intake process. By treating states as objects containing their own transition logic, we achieved the flexibility needed for rapid development while maintaining a clear, debuggable code structure.

 

One key insight was recognizing that errors are states, not exceptions which fundamentally changed how we approached error handling and recovery, leading to more robust user experiences. Combined with server-side data persistence, we created a maintainable system that handled complex asynchronous AI workflows while remaining easy to modify and extend.

 

For ServiceNow developers facing similar multi-step form challenges, this modified State Pattern offers a practical, proven approach that balances simplicity with flexibility. The pattern's strength lies not in rigid adherence to classical design patterns, but in adapting proven concepts to solve real-world problems with modern tools and constraints.

3 Comments