International JavaScript Conference https://javascript-conference.com/ Wed, 16 Sep 2026 11:51:22 +0000 en-US hourly 1 https://wordpress.org/?v=7.1 https://javascript-conference.com/wp-content/uploads/2017/03/ijs-favicon-64x64.png International JavaScript Conference https://javascript-conference.com/ 32 32 Agentic UI: Building User Interfaces for AI Agents https://javascript-conference.com/blog/agentic-ui-ai-agents-user-interfaces/ Wed, 16 Sep 2026 09:46:35 +0000 https://javascript-conference.com/?p=1079793 Agentic UI connects AI agents with dynamic user interfaces and changes how people interact with software. Instead of treating AI as a chatbot placed next to an existing application, agentic systems can actively support workflows, prepare actions, display contextual information, and adapt the interface to the user’s current goal.

The post Agentic UI: Building User Interfaces for AI Agents appeared first on International JavaScript Conference.

]]>

In a conversation with Michael Dowden, Google Developer Expert and Angular specialist Manfred Steyer discusses how agentic applications can be designed without sacrificing architectural clarity, safety, or user control. A central idea is that chat should not automatically be considered the default interface for AI-powered applications.

A Chatbot Is Not the End Goal

Many AI applications currently follow the same interaction pattern: a user enters a prompt, a large language model processes it, and the application returns a text response.

Note: This video was generated using AI, adapting the original content and technical insights created by the author of the blog post.

That pattern works well when users primarily need information. It becomes less effective when an AI system is expected to perform tasks.

An agent may need to analyze data, trigger workflows, use external tools, prepare a dashboard, or execute a sequence of actions. In these situations, forcing every interaction through a conversational interface can make the experience unnecessarily complicated.

The important distinction is not whether an application includes a chat box. It is whether the system understands the user’s goal and can select the appropriate interaction model.

An agentic interface might therefore:

  • assemble a dashboard,
  • visualize relevant information,
  • display interactive controls,
  • prepare the next step in a workflow,
  • or collaborate with the user across several stages of a task.

The user interface does not disappear. Instead, it becomes an increasingly important part of how the agent communicates its state and results.

From Text Responses to Dynamic Interfaces

Text is not always the best way to communicate information.

If a user wants to compare datasets, a table may communicate the result more effectively than several paragraphs. If parameters need to be adjusted, sliders, input fields, or selection controls may provide a better interaction model.

The same applies to complex collections of metrics. A dynamically assembled dashboard can often provide more context than a purely conversational response.

This leads toward generative or adaptive user interfaces: interfaces that can change at runtime depending on the user’s goal, the available data, and the current state of the task.

However, dynamic interfaces also create an architectural problem. If the frontend communicates directly with one particular AI framework, backend implementation, or LLM provider, the user interface can become tightly coupled to technologies that may change quickly.

Open standards such as AG-UI and A2UI are intended to address this separation.

What Is AG-UI?

AG-UI provides a standardized communication layer between an AI agent and the frontend.

Its primary architectural benefit is decoupling.

The frontend should not need to understand which agent framework, backend technology, or large language model is operating behind the application. Instead, the frontend and the agent communicate through defined messages and events.

These interactions can represent activities such as:

  • starting or completing an agent run,
  • streaming text,
  • invoking tools,
  • reporting state changes,
  • returning action results,
  • or emitting events that require a frontend response.

This separation makes it possible to change the technology behind an agent without rebuilding the entire user interface.

That is particularly relevant for enterprise applications because the AI ecosystem is evolving rapidly. An architecture that is tightly coupled to a specific provider or agent framework can quickly become difficult to maintain. AG-UI establishes a clearer boundary between the agent layer and the application interface.

Before you continue…

The reading list you'd build – if you had time.

Weekly
Articles + tutorials

The reads you'd find if you had time

2× / mo
Live webinars

Experts you can actually ask

Monthly
Magazine + whitepapers

Deep dives worth your weekend

On-demand
Recordings + courses

Past conferences, ready when you are

What Is A2UI?

AG-UI addresses communication between agents and applications. A2UI addresses a related challenge: how an agent can request an appropriate user interface.

Instead of allowing a language model to generate arbitrary HTML or JavaScript, the agent can produce a structured description of the interface it needs.

The frontend can then map that description to components that already exist within the application’s design system.

For example, an agent might determine that a user’s request is better represented as a dashboard rather than a paragraph of text. It can describe the required structure and data, while the frontend remains responsible for rendering approved components.

This provides an important balance.

The AI gains the flexibility to select an appropriate interface, but the application still controls which components exist and how they behave. The model operates within predefined boundaries instead of receiving unrestricted control over frontend code.

AI Agent Autonomy Needs Clear Boundaries

The more actions an agent can perform, the more important safety becomes.

A text-generating model has comparatively limited direct impact. An agent that can invoke tools, modify data, or initiate business processes can have consequences beyond the conversation itself.

For this reason, relying exclusively on model-level guardrails is not enough.

A stronger architectural approach is to place deterministic boundaries around a non-deterministic system.

The application should define which tools and actions are available to the agent and under which conditions they can be used. The agent can operate flexibly within those constraints, but it should not receive unrestricted access to the underlying system.

In this model, software architecture itself becomes part of the safety mechanism.

Human-in-the-Loop Does Not Mean Confirming Every Action

Human-in-the-loop patterns are often associated with confirmation dialogs.

An agent proposes an action, the application asks whether the user is sure, and the user confirms or rejects it.

That approach becomes impractical for longer agentic workflows. Requiring approval for every intermediate decision removes much of the benefit of delegating work to an autonomous agent.

A more useful model is to provide visibility and meaningful opportunities for intervention.

For example, an interface can use action cards to show what an agent is doing or has already done. Instead of requiring prior approval for every action, the application might offer an undo mechanism.

Users should also have an obvious way to stop an agent when a workflow appears to be moving in the wrong direction.

Human oversight therefore becomes less about continuous approval and more about transparency, intervention, and recovery.

Why Agentic Applications Need Continuous Feedback

A long-running agent should not leave the user looking at a loading indicator without any indication of what is happening.

During a complex task, users need enough information to understand whether the system is still working, what major actions have occurred, and whether intervention is possible.

This does not mean exposing every internal reasoning step produced by an AI model. The interface should instead communicate meaningful application-level activity.

For example, users may need to know:

  • what the agent is currently doing,
  • whether an action has changed data,
  • what stage the workflow has reached,
  • and whether they can stop or modify the process.

Feedback is therefore not merely a cosmetic UX feature. It is an essential part of creating understandable and trustworthy agentic systems.

Feedback Loops Instead of a Black Box

Agentic applications differ from conventional software because their behavior is not completely predetermined.

Traditional application logic is largely deterministic: known inputs and states should produce predictable outcomes. Large language models are probabilistic.

That does not mean the entire application has to become unpredictable.

Reliable software architecture can surround probabilistic AI behavior with deterministic rules, interfaces, and constraints.

Feedback loops are a central part of this model.

An agent performs an action, receives the resulting state, evaluates that result, and decides what to do next. At the same time, the application and the user can verify that the workflow continues to operate within the permitted boundaries.

The interaction therefore becomes an iterative cycle of goal, action, result, and adjustment rather than a single request followed by a single response.

How Should Agentic UIs Be Tested?

Agentic applications also require a different testing strategy.

Testing for an exact text response from an LLM is often fragile because the underlying model may produce different but equally valid outputs.

The more useful question is whether the application behaves correctly.

Developers can test questions such as:

  • Can the agent access the intended function?
  • Does the application render the correct component?
  • Can the user interact with the generated interface?
  • Does the agent remain inside the defined architectural boundaries?

Steyer therefore highlights black-box testing at the component level. The focus is placed on observable application behavior rather than internal implementation details or identical model responses.

For Angular applications, Vitest is also relevant to this testing approach.

The model itself does not have to produce deterministic text. The application’s rules, boundaries, and observable outcomes should remain testable.

Agentic UI Changes the Role of the Frontend

Agentic AI is not exclusively a backend concern.

As soon as agents interact directly with users, the frontend becomes part of the agentic architecture.

It has to represent state, render dynamic components, display tool results, communicate agent activity, provide intervention mechanisms, and return user feedback to the agent.

At the same time, the frontend should remain separated from the implementation details of the agent backend.

Standards such as AG-UI provide a communication boundary, while approaches such as A2UI offer a structured way to describe dynamic user interfaces.

The result is not software in which AI replaces the frontend. Instead, AI agents, conventional application logic, and user interfaces each take responsibility for the tasks they are best suited to perform.

Before you continue…

The reading list you'd build – if you had time.

Weekly
Articles + tutorials

The reads you'd find if you had time

2× / mo
Live webinars

Experts you can actually ask

Monthly
Magazine + whitepapers

Deep dives worth your weekend

On-demand
Recordings + courses

Past conferences, ready when you are

The Goal Is Not More AI in the Interface, It Is a Better Interface

The larger product question is not how to add an AI chat box to every application.

Developers instead need to determine where natural-language interaction is useful and where structured interfaces provide a better experience.

That requires answering architectural and product questions such as:

  • Where should natural language be used?
  • Where does the user need a structured interface?
  • Which decisions can an agent make autonomously?
  • When should a user be able to intervene?
  • How can the frontend remain independent from the agent implementation?
  • How can a probabilistic AI system be integrated into a reliable application?

Some of the most interesting AI-native applications may therefore emerge precisely where the traditional chatbot interaction ends.

Agentic UI is not simply about placing AI inside an existing interface. It requires developers to reconsider how people, software, and autonomous agents collaborate to complete tasks.

The post Agentic UI: Building User Interfaces for AI Agents appeared first on International JavaScript Conference.

]]>
Convince Your Boss: iJS Conference Munich 2026 Business Case https://javascript-conference.com/blog/convince-your-boss-ijs-conference-munich-2026-business-case/ Wed, 09 Sep 2026 11:42:24 +0000 https://javascript-conference.com/?p=1079735 AI agents are moving quickly from experiments and coding assistants into real software development workflows. That changes more than developer productivity: it affects architecture, application state, security, compliance, testing, and the way teams maintain their codebases. That is exactly where iJS Munich 2026 comes in. From 26–30 October 2026, iJS Munich will take place as part of Agentic Web Week Munich, bringing iJS, IPC, and webinale together for one week of workshops, sessions, and practical exchange around modern software development. If you would like to attend but still need approval from your manager, this article gives you the arguments you need to build a strong business case.

The post Convince Your Boss: iJS Conference Munich 2026 Business Case appeared first on International JavaScript Conference.

]]>
.wrappermlcBasedButton686136.btnWrapper{ text-align:left; } .mlcBasedButtons686136.gdlr-button{ color:#ffffff!important; background-color: #6610f2!important; margin-bottom: ; } .mlcBasedButtons686136.gdlr-button:hover{ color:#ffffff!important; background-color: #6610f2!important; } @media only screen and (max-width: 767px) { .btnWrapper{ text-align: center !important; } }

Why is iJS Munich 2026 particularly relevant this year?

The question for development teams is no longer whether AI will influence software engineering. It already does.

The more important question is how teams can use AI agents without sacrificing architecture, predictability, security, and maintainability.

At iJS Munich 2026, agentic development is therefore not treated as a collection of isolated AI demos. The program looks at what happens when agents become part of real engineering workflows: when they can generate code, interact with applications, use tools, work with shared state, and make changes that have consequences beyond a single prompt.

For you and your team, this means gaining practical guidance at the point where many organizations are still defining their own standards.

Instead of discovering the limitations of agentic development through production incidents, unmaintainable code, or security problems, you can learn from concrete patterns and approaches that are already emerging across the industry.

How can you move AI agents from experiments to engineering discipline?

AI coding tools can produce code quickly. The harder problem is making sure that code still respects the architecture and conventions of your application.

Several topics at iJS Munich address exactly this challenge.

Sessions around WebMCP, MCP Apps, AI-assisted Angular development, shared application state, and agentic user interfaces explore how agents can interact with software in a more controlled and predictable way.

One of the central ideas is simple: if an agent is going to work inside your codebase, it needs rules.

Architecture boundaries, state-management conventions, permitted actions, and UI constraints cannot exist only as knowledge in the heads of experienced developers. They increasingly need to become explicit enough that both humans and AI-supported development workflows can follow them.

For teams already experimenting with coding agents, this can help answer questions such as:

  • Which parts of an application can an agent change independently?
  • Where do we need strict architectural contracts?
  • How should an agent interact with shared application state?
  • How can we prevent AI-generated functionality from bypassing established patterns?
  • When should an agent generate an interface freely, and when should it operate inside a predefined structure?

The Agentic UI with Angular workshop adds a hands-on dimension, giving participants the opportunity to work directly with agent-driven interfaces rather than only discussing them conceptually.

The result is knowledge that can be transferred back into actual engineering guidelines, architecture decisions, and development workflows.

Before you continue…

The reading list you'd build – if you had time.

Weekly
Articles + tutorials

The reads you'd find if you had time

2× / mo
Live webinars

Experts you can actually ask

Monthly
Magazine + whitepapers

Deep dives worth your weekend

On-demand
Recordings + courses

Past conferences, ready when you are

What changes in security once agents can read, write, and act?

Giving an AI system more capabilities also creates a larger attack surface.

An agent that can access application data, call tools, work with credentials, modify information, or trigger actions needs a very different level of control than a chatbot that only returns text.

That is why security is one of the strongest arguments for attending iJS Munich 2026.

The program covers both established web security practices and risks introduced or amplified by AI-assisted development.

Topics include the OWASP Top Ten 2025, HTTP security headers, privacy and security risks in AI development tools, and the architectural implications of the EU AI Act.

For development teams, the relevance is very practical.

AI coding tools may receive proprietary source code, credentials, internal information, or sensitive context. Agentic applications may also be able to execute actions that previously required a human user.

Understanding where information can leak, which permissions agents should receive, and how compliance requirements affect system architecture can help teams establish safeguards before an incident occurs.

A preventable security or privacy problem can easily cost significantly more than conference attendance. Learning how to recognize these risks early is therefore not only a technical benefit but a business one.

Which React, Angular, and Next.js developments can you bring back to your projects?

AI may be the headline topic, but the frontend frameworks teams use every day are continuing to evolve as well.

iJS Munich 2026 includes practical sessions covering current developments in Angular, React, and Next.js, with a particular focus on state management, performance, data handling, and architecture.

Angular developers can explore topics such as the Resource APIs and modern approaches to state management, including when to use NgRx, when Signal Store is appropriate, and how the two can work together.

React teams can look at approaches for creating more deterministic relationships between data and UI state.

For Next.js developers, multi-tenant architecture brings another set of practical questions around tenant isolation, routing, and incremental static regeneration strategies.

These are not only topics for long-term technology roadmaps. They can influence architecture reviews and pull requests immediately after the conference.

That makes iJS Munich useful both for teams exploring the next generation of AI-assisted development and for teams focused on improving the frontend applications they already operate today.

Why does engineering craft become even more important with AI-generated code?

Agentic tools amplify the quality of the environment in which they operate.

If a codebase has clear boundaries, understandable architecture, consistent conventions, and manageable dependencies, an AI-supported workflow has a much better foundation.

If a codebase is already difficult to understand, agents can multiply that complexity just as quickly.

That is why the iJS Munich program also looks beyond AI tooling itself.

Sessions address topics such as:

  • why development teams overengineer solutions,
  • what makes architecture understandable to both AI agents and new team members,
  • how to modernize legacy software incrementally,
  • and how to design production systems that can recover from incidents without losing data.

The underlying lesson is important: AI-friendly architecture and human-friendly architecture have a great deal in common.

Clear modules, explicit responsibilities, predictable data flows, and well-defined boundaries make software easier for experienced developers, new colleagues, and AI-supported tools alike.

For a manager, that makes conference attendance easier to justify. The goal is not simply to learn another AI tool. It is to improve the engineering practices that determine whether those tools create sustainable productivity or additional technical debt.

What does the whole team gain when one person attends?

A conference should create more value than the experience of a single attendee.

A strong business case therefore includes a concrete plan for transferring knowledge back into the team.

After iJS Munich, you could turn what you learned into:

  • an internal recap of the agentic engineering patterns most relevant to your codebase,
  • a short briefing on AI security, privacy, and EU AI Act considerations,
  • recommendations on which React, Angular, or Next.js developments are worth adopting,
  • a small pilot or proof of concept for an agentic workflow,
  • or updated team guidelines for AI-assisted development.

This turns one conference ticket into structured input for the entire development team.

Recorded sessions extend that value further. Colleagues can revisit relevant material after the conference instead of relying solely on notes or second-hand summaries.

What is the advantage of the Agentic Web Week Full Week Pass?

iJS Munich takes place within Agentic Web Week Munich, which brings together iJS, IPC, and webinale.

For teams whose work extends beyond JavaScript alone, the Full Week Pass can therefore offer an even broader return.

It includes workshops across all three conferences as well as recordings of 100+ sessions, creating access to expertise beyond the sessions one attendee can physically visit during the week.

That can be particularly valuable for cross-functional development teams working across frontend, architecture, AI, security, backend systems, and web technologies.

Instead of purchasing isolated training for individual topics, the week provides the opportunity to explore several connected challenges in one place.

What makes the workshops especially valuable?

Conference sessions are useful for orientation and new ideas. Workshops provide the time to apply those ideas in practice.

The program surrounding Agentic Web Week includes hands-on formats covering areas such as agentic UI development, LangChain, enterprise AI engineering, and web application performance.

Rather than leaving with only a list of tools to investigate later, workshops give you an opportunity to work through concepts, understand their limitations, and evaluate how they could fit your own projects.

This makes it easier to return to your team with something more concrete than inspiration: patterns, prototypes, architectural ideas, and practical next steps.

How can you convince your manager?

The strongest argument for attending iJS Munich 2026 is not simply that the conference covers current technologies.

It is that several decisions development teams are making right now are becoming increasingly difficult to separate.

Adopting AI coding tools is also an architecture decision.

Giving agents access to application functions is also a security decision.

Using proprietary code as AI context is also a privacy and governance decision.

Updating Angular, React, or Next.js affects both development productivity and maintainability.

And improving a codebase for AI-supported development often means improving it for human developers at the same time.

iJS Munich brings these questions together in one program and gives you the opportunity to evaluate them before committing your team to tools, architectures, or workflows that may be difficult to reverse later.

The post Convince Your Boss: iJS Conference Munich 2026 Business Case appeared first on International JavaScript Conference.

]]>
AG-UI: The Standard For Agentic User Interfaces https://javascript-conference.com/blog/ag-ui-agentic-user-interfaces-angular/ Tue, 11 Aug 2026 11:43:58 +0000 https://javascript-conference.com/?p=1079653 In this article, I'll describe how AG-UI works and use an example to show how this standard can be used in Angular to connect agents that execute server- and client-side tools and answer questions using dynamic components.

The post AG-UI: The Standard For Agentic User Interfaces appeared first on International JavaScript Conference.

]]>
In practice, Agentic AI often leads to tightly coupled systems: the UI and agent logic in the backend are directly interconnected and depend on each other. This complicates maintenance, reduces flexibility, and causes vendor lock-in. AG-UI solves this problem by defining a clear, message-based interface between the frontend and the agent. This results in a decoupled architecture independent of backend technologies and LLM providers.

The source code used for this can be found here on GitHub in the agentic branch.

Agentic AI and the Frontend

Our demo application can be used entirely without AI integration. However, if users get stuck or want to streamline workflows, they can activate a sidecar and chat with an LLM using a server-side agent (Fig. 1).

Fig. 1: Sample application

Fig. 1: Sample application

The LLM can utilize tools that the sidecar provides to answer questions. On the backend, it can access databases and it handles tasks related to stores, forms, and routing on the frontend.

The LLM can also select components from a catalog, which the Sidecar displays. It passes data determined by the tools to these components.

Let’s look at the two tools used in the example. getBookedFlights is a server-side tool that retrieves the current user’s flights. The findFlights tool initiates a flight search on the client side. It navigates to the page with the search form, fills it out, and triggers the search.

For transparency, it’s good practice to inform users about steps taken like tool calls. The example makes this very explicit by also outputting the internal tool names. In a business application, this would be phrased in slightly less technical terms. Instead of “Tool Call: getBookedFlights,” the sidecar could write “Determine booked flights…” in the chat history.

Besides the tool calls, the chat also displays textual responses and a flight map selected by the LLM. The sidecar passes the flight that will be displayed to this map. The Angular application accesses the agent via HTTP, which has access to one (or more) LLM(s) (Fig. 2). An agent client handles communication details, informing the agent about the client-side tools and components. The agent also maintains a list of server-side tools.

Fig. 2: Architecture

Fig. 2: Architecture

When the agent forwards the user’s request to the LLM, it also sends the collected information about server-side tools, client-side tools, and components. The LLM determines what additional data it needs and requests tool calls. The agent handles server-side tool calls and delegates client-side tool execution to the agent client. It reports its results back to the LLM, which continues processing its task.

To provide the final response to the query, the LLM returns free-form text. It can also send a JSON document specifying which components the sidecar should display. The values for these components’ inputs are also included in the JSON. Depending on the LLM’s capabilities, this JSON document can be a structured response or parameters passed to a client-side tool.

The structured response-also called Structured Output-is the cleaner solution from a semantic perspective. Many LLMs support a formal description of parameters for tool calls using JSON Schema. Because of this, using tool calling to display components is often the more pragmatic solution. This raises the question of how communication with the agent should be designed without creating too strong a coupling with the client. The AG-UI standard provides answers.

Before you continue…

The reading list you'd build – if you had time.

Weekly
Articles + tutorials

The reads you'd find if you had time

2× / mo
Live webinars

Experts you can actually ask

Monthly
Magazine + whitepapers

Deep dives worth your weekend

On-demand
Recordings + courses

Past conferences, ready when you are

What is AG-UI?

To stay independent of server technologies and avoid vendor lock-in with individual LLM providers, the AG-UI standard defines the message types for communication between client and agent. The creators of CopilotKit, a convenient frontend SDK for developing AI-based assistants and sidecars, are behind the standard. Adapters now exist for virtually all well-known agent frameworks. At the time of writing, the AG-UI website listed the following supported frameworks:

  • AG2
  • Agno
  • AWS Bedrock AgentCore
  • AWS Bedrock Agents
  • AWS Strands Agents
  • Cloudflare Agents
  • CrewAI
  • Google ADK
  • LangGraph
  • LlamaIndex
  • Mastra
  • Microsoft Agent Framework
  • OpenAI Agent SDK
  • Pydantic AI

AG-UI defines messages exchanged between the client and the agent. These messages describe the transmission of text or tool calls along with their results, for example. Subsequent messages can provide further details based on information from previous messages. This forms the basis for streaming text and parameters for tool calls.

AG-UI is deliberately designed to be transport-agnostic, so it makes no assumptions about the underlying transport protocol. Typically, HTTP with Server-Sent Events (SSE) or WebSockets are used.

Message Types in AG-UI

For individual messages exchanged between the agent and the client, AG-UI defines types that can be subdivided into different categories. Table 1 shows a selection of message types that we’ll use in this article.

Category Message Type Description
Lifecylce RUN_STARTED Starts a run that contains all the messages needed to answer a question. These messages include text messages and ones that describe tool calls.
RUN_FINISHED Completes a run.
RUN_ERROR Reports an error for a run.
Text Message TEXT_MESSAGE_START Start by sending a text message.
TEXT_MESSAGE_CONTENT Provides (additional) parts of the text message.
TEXT_MESSAGE_END Ends a text message.
Tool Call TOOL_CALL_START Initiates a tool call.
TOOL_CALL_ARGS Provides (additional) parameters for the tool call.
TOOL_CALL_END Ends the tool call.
TOOL_CALL_RESULT Returns the result of a tool call.

Table 1: Selected message types in AG-UI

According to the terminology, a run begins with a user’s question. It encompasses all messages that the agent sends in response. This includes messages related to tool calls, results of tool calls, and textual responses. For instance, two runs can be identified in Fig. 1 at the beginning. The first begins with the question about a flight to France, and the second with the search query for flights from Graz to London.

To enable streaming, AG-UI lets most information be split into multiple messages. Examples of this are the message types TEXT_MESSAGE_CONTENT and TOOL_CALL_ARGS. Multiple messages of these types can gradually provide text or additional arguments for a tool call.

The messages in Listing 1, which reflect the first run from Figure 1, show AG-UI usage.

Listing 1

{"type":"RUN_STARTED", "threadId":"f66a", "runId":"95e2"}

{"type":"TOOL_CALL_START", "toolCallId":"3PQX", "toolCallName":"getBookedFlights"}

{"type":"TOOL_CALL_ARGS", "toolCallId":"3PQX", "delta":"{}"}

{"type":"TOOL_CALL_END", "toolCallId":"3PQX"}

{"type":"TOOL_CALL_RESULT", "toolCallId":"3PQX", "content":"...JSON...", "role":"tool"}

{"type":"TEXT_MESSAGE_START","messageId":"d110","role":"assistant"}

{"type":"TEXT_MESSAGE_CONTENT","messageId":"d110","delta":"Yes - you already booked "}

{"type":"TEXT_MESSAGE_CONTENT","messageId":"d110", "delta":"a flight to France."}

{"type":"TEXT_MESSAGE_END","messageId":"d110"}

{"type":"TOOL_CALL_START", "toolCallId":"TjaS", "toolCallName":"showComponents"}

{"type":"TOOL_CALL_ARGS", "toolCallId":"TjaS", "delta":"...JSON..."}

{"type":"TOOL_CALL_END", "toolCallId":"TjaS"}

{"type":"RUN_FINISHED", "threadId":"f66a", "runId":"95e2"}

These messages describe a tool call for the server tool getBookedFlights and for the client tool showComponents, as well as a textual response. For readability, the IDs-which are typically GUIDs-have been truncated to 4 digits, and the JSON embedded in strings containing arguments and tool results is only hinted at. Thanks to the IDs, we can trace which initiated tool call the supplied arguments or logged results belong to.

To make sure that initial content can be displayed even during transmission, the agent splits its response into two text messages. In practice, this division is often even more granular. Similar to tool calls, messages that form a response together are assigned the same ID.

The two lifecycle messages that start and end the run contain the runId and a threadId. This is used to group all runs in a chat history.

Before you continue…

The reading list you'd build – if you had time.

Weekly
Articles + tutorials

The reads you'd find if you had time

2× / mo
Live webinars

Experts you can actually ask

Monthly
Magazine + whitepapers

Deep dives worth your weekend

On-demand
Recordings + courses

Past conferences, ready when you are

AG-UI SDK

So that we don’t have to start from scratch, AG-UI comes with a protocol specification and with SDKs for TypeScript and Python. Many adapters for agent frameworks are built on top of these. The Microsoft Agent Framework comes with its own implementation to support AG-UI on the server side via C# as well. In addition, the official AG-UI repository also contains community implementations for other languages like Java and C++ and frameworks like Spring AI.

The official SDKs for TypeScript and Python support HTTP via Server-Sent Events: Each run triggers an HTTP request, and then the server-side agent gradually sends its response containing individual text messages and tool calls to the client. These messages can be encoded using JSON or, in binary format, via Protocol Buffers.

TypeScript SDK on the Server

To illustrate the server-side use of the TypeScript SDK, Listing 2 shows a simple hard-coded example that sends simple AG-UI-compliant messages without using a language model. To do this, the server follows an old rule of small talk: it’s never wrong to talk about the weather.

Listing 2

import { HttpAgent } from '@ag-ui/client';

import { BaseEvent, EventType, RunAgentInput } from '@ag-ui/core';

import { Observable } from 'rxjs';

export class FlightWeatherAgent extends AbstractAgent {

run(input: RunAgentInput): Observable<BaseEvent> {

return new Observable((observer) => {

const { threadId, runId } = input;

observer.next({ type: EventType.RUN_STARTED, threadId, runId });

observer.next({

type: EventType.TEXT_MESSAGE_START,

messageId: '1001',

role: 'assistant',

});

observer.next({

type: EventType.TEXT_MESSAGE_CONTENT,

messageId: '1001',

delta: 'Checking flight weather for Frankfurt...',

});

observer.next({ type: EventType.TEXT_MESSAGE_END, messageId: '1001' });

\[…\]

observer.next({ type: EventType.RUN_FINISHED, threadId, runId });

observer.complete();

});

}

}

The FlightWeatherAgent shown here inherits from AbstractAgent. The run method processes user requests. It starts a run, sends a text message to the client, and then terminates the run.

Typically, run delegates to an agent framework such as Mastra, LangGraph, Google ADK, Microsoft Agent Framework, or Spring AI, and converts the received information into AG-UI-compliant messages. You usually don’t have to write this integration yourself, especially since the AG-UI SDK provides adapters for many frameworks, and some frameworks even have their own integration.

Note that the SDK does not handle the connection to the selected transport protocol-for example, sending SSE via HTTP. That’s why the included demo project also contains a small piece of glue code that logs in to the agent and sends all messages as SSE.

TypeScript SDK on the Client

On the client side, the TypeScript SDK lets us define an AgentSubscriber with an event handler for incoming messages (Listing 3).

Listing 3

import { AgentSubscriber } from '@ag-ui/client';

const subscriber: AgentSubscriber = {

onRunStartedEvent: ({ event }) => {

console.log(

\`RUN_STARTED: threadId=\${event.threadId}, runId=\${event.runId}\`,

);

},

onTextMessageStartEvent: ({ event }) => { \[…\] },

onTextMessageContentEvent: ({ event }) => { \[…\] },

onTextMessageEndEvent: ({ event }) => { \[…\] },

\[…\]

onRunFinishedEvent: ({ event }) => { \[…\] },

};

A separate handler is provided for each message type. Using the HttpAgent, which the SDK also provides, you can establish a connection to the agent on the server side (Listing 4).

Listing 4

import { FlightWeatherAgent } from './server.js';

const threadId = '4711';

const url = 'https://...';

const agent = new HttpAgent({ url, threadId });

const userMessage = {

id: 'msg-user-1',

role: 'user' as const,

content: 'What is the flight weather in Frankfurt?',

};

agent.addMessage(userMessage);

await agent.runAgent({ runId: '0815' }, subscriber);

The addMessage method initially adds the userMessage only to a client-side array. The locally collected messages are transmitted only when runAgent starts the next run. As soon as the responses from the agent arrive via SSE, runAgent calls the corresponding handlers in the passed-in AgentSubscriber.

Depending on whether the agent remembers the conversation history between individual runs, the client either sends all messages exchanged so far or only the newly added ones.

Server-Side Tool Calling

So far, our demo has only sent simple text messages. However, the agent’s process for requesting tool calls is very similar. Typically, information to be sent also comes from the chosen language model and the agent formats it in accordance with the AG-UI. For simplicity’s sake, we’ll continue using a few hard-coded messages in our demo (Listing 5).

Listing 5

// Server Code

// Step 1: Tool Call

observer.next({

type: EventType.TOOL_CALL_START,

toolCallId: '2001',

toolCallName: 'loadFlightWeather',

});

observer.next({

type: EventType.TOOL_CALL_ARGS,

toolCallId: '2001',

delta: '{"city":"Frankfurt"}',

});

observer.next({

type: EventType.TOOL_CALL_END,

toolCallId: '2001'

});

// Step 2: Execute Server-side Tool

const weatherResult = \[…\];

// Step 3: Answer Tool Call

observer.next({

type: EventType.TOOL_CALL_RESULT,

toolCallId: '2001',

messageId: '3001',

role: 'tool',

content: JSON.stringify(weatherResult),

});

With the first three messages, the agent indicates that the LLM has requested a tool call. Since this is a server-side tool, the agent executes it and returns the result to the language model. It also logs the result via an additional AG-UI message.

Based on these messages, the client can inform users about the tool call. This creates transparency and prevents prolonged downtime in the UI.

Client-Side Tool Calling

Client tools are executed similarly to server tools. Here, too, the agent provides information about the call and its parameters via the relevant tool call messages. The difference is that the client responds to these messages by executing a tool and returning the result in the next run.

However, this presents a challenge at first. The language model must be informed about the available client tools. The TypeScript SDK comes to our aid for this task. It has a Tool data type that can be used to define the client tools. In addition to a name and a textual description, this type also includes information about the expected parameters (Listing 6).

Listing 6

// Client Code

import { Tool } from '@ag-ui/client';

import { z } from 'zod';

const weatherSchema = z.object({

condition: z.string()

.describe('e.g., sunny, cloudy, rainy.'),

temperature: z.string()

.describe('e.g., 25°C, 77°F.'),

wind: z.string()

.describe('e.g., 5 km/h, 3 mph.'),

});

export const showWeatherTool: Tool = {

name: 'showWeather',

description: 'Provide weather data the client can render.',

parameters: z.toJSONSchema(weatherSchema)

};

The parameters must be defined as a JSON schema. Our example uses the popular zod schema library to provide this schema. Based on the tool’s description and the parameters, the LLM can decide when and how to invoke the tool.

The runAgent method takes the tool descriptions and sends them to the agent (Listing 7).

Listing 7

// Client Code

// 1st run

await agent.runAgent(

{ runId: '0815', tools: \[showWeatherTool\] },

subscriber);

// Look into received client-side tool calls

// and perform respective actions

const toolCallResultMessage = \[…\];

// Add Tool Call Result

agent.addMessage(toolCallResultMessage);

// 2nd run

await agent.runAgent(

{ runId: '0816', tools: \[showWeatherTool\] },

subscriber);

After each run, the client checks whether the AgentSubscriber has received requests for client-side tool calls. If so, it executes those tools and sends the results back to the agent as part of another run.

AG-UI, the message history, and client tools

Messages stored via addMessage, as well as the information regarding the client tools, are transmitted via the payload of the HTTP request that triggers the next run. Unlike the messages discussed, the AG-UI protocol does not define this payload’ structure. It’s merely an implementation detail of the AG-UI SDK. Given the official nature of this SDK, hopefully other implementations will follow this model.

Reading information about client tools on the server

As mentioned earlier, the adapters of individual agent frameworks forward the received information about client tools to the LLM. If you wish to manually handle this information in the agent, you can find it in the tools property of the parameter object passed to run (Listing 8).

Listing 8

class FlightWeatherAgent extends AbstractAgent {

run(input: RunAgentInput): Observable<BaseEvent> {

return new Observable((observer) => {

console.log('tools', input.tools);

\[…\]

});

}

}

This is a JSON schema containing the parameter descriptions and textual information that the client defined using Zod (Fig. 3).

Fig. 3: JSON schema for client tools

Fig. 3: JSON schema for client tools

Integration into Angular

The AG-UI SDK provides a solid foundation for integration into Angular. But you need an extra abstraction layer in order to use it conveniently, without unnecessary boilerplate code. The demo project contains this kind of abstraction in the libs/ag-ui-client folder. To simplify imports, the tsconfig.json file contains a path mapping @internal/ag-ui-client that points to it.

The linchpin of this implementation is an agUiResource for communicating with the agent. It also provides a WidgetContainer component that displays components requested by the agent, like the flight map shown at the beginning. The underlying API design is heavily inspired by the Hashbrown framework, which, from an Angular developer’s perspective, allows for an extremely idiomatic approach to working with LLMs, but currently doesn’t include AG-UI integration yet.

This article treats this abstraction as a black box that should eventually be provided by a library. Feel free to use the source code as a basis for your own projects.

By using AG-UI, the Angular example is independent of server-side technologies and models. To make execution as simple as possible, the demo application includes an agent that uses the extremely convenient TypeScript-based agent framework Mastra. Since the AG-UI SDK provides an adapter for Mastra, agents developed with it can be easily integrated via AG-UI.

The client has been tested with both OpenAI’s GPT-5 and Google’s Gemini 3. Details on setting up and running the demo can be found in the README.

Before you continue…

The reading list you'd build – if you had time.

Weekly
Articles + tutorials

The reads you'd find if you had time

2× / mo
Live webinars

Experts you can actually ask

Monthly
Magazine + whitepapers

Deep dives worth your weekend

On-demand
Recordings + courses

Past conferences, ready when you are

Connecting agents via agUiResource

The agUiResource provided by @internal/ag-ui-client implements Angular’s Resource API and can be used similarly to the typical httpResource or rxResource (Listing 9).

Listing 9

import { defineAgUiComponent } from '@internal/ag-ui-client';

\[…\]

private readonly chat = agUiResource({

url: '<http://localhost:3001/ag-ui/ticketingAgent>',

useServerMemory: true,

tools: \[

findFlightsTool,

getLoadedFlightsTool,

toggleFlightSelectionTool,

getCurrentBasketTool,

displayFlightDetailTool,

createShowComponentsTool(\[

flightWidgetComponent

\]),

\],

});

The properties refer to the agent’s URL. With useServerMemory, the caller specifies whether the agent should save the chat history. If not, the client must repeat the entire chat history in every call.

In the tools list, the consumer registers all client-side tools that the agent is permitted to request. One special tool is showComponents, which is created via a factory. This factory accepts the description of possible components that the agent can integrate to answer questions. In our case, only a flight map (flightWidget) is available.

The individual tools can be described using the defineAgUiTool function (Listing 10).

Listing 10

import { findFlightsTool } from '@internal/ag-ui-client';

import { z } from 'zod';

\[…\]

export const findFlightsTool = defineAgUiTool({

name: 'findFlights',

description:

\`Searches for flights and redirects the user to the result\`,

schema: z.object({

from: z.string().describe('airport of departure'),

to: z.string().describe('airport of destination'),

}),

execute: async (args) => {

const store = inject(FlightStore);

const router = inject(Router);

store.updateFilter(args.from, args.to);

await router.navigate(\['/ticketing/booking/flight-search'\]);

},

});

Besides a name, a description, and parameter definitions based on Zod, the tool definition also includes an execute method. When the agent requests a tool, the agUiResource executes this method.

The parameter object args is of the TypeScript type defined by the Zod schema. In our case, this is an object with the properties from and to. The implementation shown triggers a flight search by passing these search criteria to the FlightStore and navigates users to the results page.

Tools that retrieve data-such as locally available states or user responses-can report this back to the model via the return value. An example of this is the getLoadedFlightsTool, which informs the agent about the flights that the application is currently displaying to the user (Listing 11).

Listing 11

export const getLoadedFlightsTool = defineAgUiTool({

name: 'getLoadedFlights',

description: \`

Returns the currently loaded/displayed flights\`

execute: () => {

const store = inject(FlightStore);

return store.flightsValue().map(toFlightInfo);

},

});

Components are defined in a similar way, but in this case the passed-in schema describes the available inputs (Listing 12). In this example, the flight property refers to another Zod schema (Listing 13).

Listing 12

import { defineAgUiComponent } from '@internal/ag-ui-client';

import { z } from 'zod';

export const flightWidgetComponent = defineAgUiComponent({

name: 'flightWidget',

description:

\`Displays a concrete flight as an interactive card. Use it when referring to one or more specific flights.\`,

component: FlightWidget,

schema: z.object({

flight: flightSchema,

status: z.enum(\['booked', 'other'\]).describe('Status of the flight'),

}),

});

**Listing 13**

const flightSchema = z.object({

id: z.number().describe('The flight id'),

from: z.string().describe('Departure city'),

to: z.string().describe('Arrival city'),

date: z.string().describe('Departure date in ISO format'),

delay: z.number().describe('Delay in minutes'),

});

To send a request to the agent, the client uses the _sendMessage_ method:

this.chat.sendMessage({

role: 'user',

content: 'Did I book my flight to France?'

});

Now the client just needs to display the response messages returned by the agent via the AG-UI in the chat history. The next section covers this.

Displaying the Chat History in the Template

The entire chat history is contained in the value of the AgUiResource. This is a signal that contains an array of AgUiChatMessages (Listing 14).

Listing 14

@for (message of chat.value(); track message.id) {

@if (message.content) {

&lt;div&gt;{{ message.content }}&lt;/div&gt;

}

@for (widget of message.widgets; track widget.id) {

&lt;app-widget-container \[widget\]="widget" /&gt;

}

@for (toolCall of message.toolCalls; track toolCall.id) {

&lt;div&gt;Tool Call: {{ toolCall.name }}&lt;/div&gt;

}

}

In addition to a textual response (content), the iterated messages also contain a list of components (widgets) and tools (toolCalls). The widgets are components that have been registered for the showComponents tool and selected by the LLM. The WidgetContainer provided by _@internal/ag-ui-clien_t can dynamically generate these widgets and display them using the properties passed by the agent or LLM.

The toolCalls list provides information about the requested tools. Entries can refer to both client-side and server-side tools and are displayed here for transparency. We don’t need to worry about executing client-side tools however, since agUiResource handles this without any further action on our part.

The streamed AG-UI messages can be tracked using the browser’s Developer Tools (Fig. 4). This information makes the log accessible and aids troubleshooting.

Fig. 4: AG-UI messages in the Dev tools

Fig. 4: AG-UI messages in the Dev Tools

 

Before you continue…

The reading list you'd build – if you had time.

Weekly
Articles + tutorials

The reads you'd find if you had time

2× / mo
Live webinars

Experts you can actually ask

Monthly
Magazine + whitepapers

Deep dives worth your weekend

On-demand
Recordings + courses

Past conferences, ready when you are

Conclusion

AG-UI consistently decouples the frontend and agent via a clearly defined, message-based interface. This eliminates direct dependencies between the UI, backend, and LLM: tool calls, streaming, and UI updates are handled via standardized messages. It reduces complexity, facilitates the replacement of agent frameworks, and prevents vendor lock-in.

The existing SDK for TypeScript handles a large part of the integration work. It provides ready-made abstractions for runs, messages, and tool calls, as well as adapters for many existing agent frameworks. On the client side, this can be used to create lightweight, framework-specific wrappers that integrate AG-UI idiomatically without having to reimplement the protocol itself.

The post AG-UI: The Standard For Agentic User Interfaces appeared first on International JavaScript Conference.

]]>
WebMCP: Bridging the Gap Between AI Agents and the Web https://javascript-conference.com/blog/webmcp-ai-agents-web-apps/ Mon, 29 Jun 2026 09:16:05 +0000 https://javascript-conference.com/?p=210121 AI agents struggle with traditional web interfaces because they rely on brittle DOM parsing and simulated user interactions. WebMCP introduces a browser-native way for applications to expose structured tools directly to agents, enabling more reliable, secure, and deterministic automation. This article explains how WebMCP works, compares it to existing approaches, and demonstrates both declarative and imperative implementation patterns.

The post WebMCP: Bridging the Gap Between AI Agents and the Web appeared first on International JavaScript Conference.

]]>
We’ve been hearing a lot of noise about AI agents lately. The industry promise is huge: autonomous or semi-autonomous systems executing complex workflows on our behalf. But if you’re a front-end architect or a senior developer who has actually tried to let an AI agent navigate a modern web application, you’ve already hit the wall. The reality under the marketing hood is that agentic actuation is incredibly brittle.

When an AI agent relies on standard web browsing modes, it’s essentially just an unimpressive scraper. It takes screenshots, parses messy, deeply nested DOM trees, and tries to guess what a button does based on heuristics, ARIA labels, ids, or CSS classes. The moment your app triggers a client-side layout shift, updates a component in your design system, or handles an asynchronous state change, the agent falls over. It gets stuck in loops or, worse, hallucinates data and submits half-baked forms.

What if our web applications didn’t have to sit passively while an external AI agent tries to brute-force its way through a user interface designed exclusively for human eyes? What if we could design interfaces that explicitly, cleanly, and securely tell AI agents exactly what they can do, how to interact with them, and what structured data they expect? This is exactly what WebMCP (Web Model Context Protocol) aims to solve.

Recently proposed as a new web standard and available as an early preview in Google Chrome, WebMCP is a game-changer for front-end engineering in the AI era. It allows us, front-end developers, to bridge the gap between our web applications and the rapidly expanding ecosystem of AI agents through a clean architecture pattern: progressive enhancement for AI.

In this article, we are going to explore the underlying mechanics of WebMCP, evaluate why it is an essential tool for the modern front-end stack, analyze its core architecture, and walk through a step-by-step implementation of both its imperative and declarative options.

Before you continue…

The reading list you'd build – if you had time.

Weekly
Articles + tutorials

The reads you'd find if you had time

2× / mo
Live webinars

Experts you can actually ask

Monthly
Magazine + whitepapers

Deep dives worth your weekend

On-demand
Recordings + courses

Past conferences, ready when you are

The Fragility of Agentic Actuation

Before we can appreciate WebMCP, we must first deeply understand the architectural limitations of how AI agents interact with the web today.

In the current landscape, when an LLM-backed agent is tasked with completing an action on a website, such as booking a flight, filling out a complex expense form, or running an application diagnostic, it relies on a process called actuation. Actuation is the act of an agent simulating manual user interactions like mouse movements, clicks, scrolls, and keyboard inputs.

To accomplish this, the agent is usually fed a serialized representation of the active page. This might be a raw HTML string, a simplified accessibility tree, or a sequence of viewport screenshots. The agent processes this data, predicts the coordinates or the DOM selectors of the element it needs to interact with, and executes the simulated input.

This approach suffers from several catastrophic architectural flaws:

1. High semantic noise: web pages are packed with elements that are critical for human visual comprehension and brand identity but represent pure noise to an LLM. Navigation menus, promotional banners, sidebars, tracking scripts, and complex CSS layouts aren’t supposed to be used by an agent. The agent wastes valuable token context window space filtering out this layout noise just to find a single form field. That means this process is costly.

2. Contextual misinterpretation: Human-first user interfaces rely heavily on visual cues and micro interactions. A date picker component might look like a simple input field to a raw HTML parser, but interacting with it requires clicking a tiny calendar icon, navigating months via pagination arrows, and selecting a specific table cell in the picker panel. An AI agent trying to actuate this component directly through simulated clicks can break the component’s internal state.

3. State desynchronization: Single Page Applications (SPAs) built with modern frameworks like React, Next.js, or Vue manage state asynchronously. When an agent clicks a button that triggers a client-side route transition or a delayed state mutation, the agent may attempt its next action before the DOM has finished re-rendering, leading to target selection errors and broken flows.

4. Lack of determinism: LLMs are inherently probabilistic. If you ask an agent to fill out a registration form ten times using raw UI actuation, there is a statistically significant chance that it will misinterpret a field label or format a piece of data incorrectly on at least one of those attempts. In enterprise, financial, or healthcare applications, a 90% success rate is an absolute failure. We require 100% determinism when executing transactions.

WebMCP tries to solve all the above. Instead of forcing the agent to infer intent from presentation, WebMCP allows the presentation layer to explicitly declare its capabilities directly to the browser runtime.

What is WebMCP?

WebMCP is an adaptation and extension of the broader Model Context Protocol (MCP) originally created by Anthropic and open-source contributors. While standard MCP focuses on connecting AI models to local development tools, databases, and enterprise secure environments via transport layers like SSE (Server-Sent Events) or stdio, WebMCP brings this protocol natively into the browser.

WebMCP exposes a structured, highly secure bridge between the web page running in a tab and the underlying AI assistant operating at the browser level. It transforms your web application from a static document that must be visually scraped into a dynamic, queryable, and executable engine of tools.

gil1

When a web page leverages WebMCP, it registers specific capabilities with the browser’s model context. The agent no longer needs to guess how to perform a task. It simply looks at the list of registered tools, reads their strictly typed definitions, and invokes them using deterministic JSON arguments.

The Three Pillars of WebMCP

To provide a robust alternative to raw UI actuation, WebMCP establishes three core pillars:

1. Standardized discovery: when a user navigates to a website, an agent operating within or alongside the browser needs a uniform way to query what operations are available on that specific page. WebMCP establishes a native browser API (navigator.modelContext) that acts as a registry. As soon as the page bootstraps, your application registers its tools. The browser can immediately communicate these capabilities to the underlying AI model without requiring the model to parse a single line of your application’s visual HTML layout.

2. Strict JSON schemas: one of the primary causes of agent failure is data formatting mismatch. For example, an agent inputting a full name into a field that strictly expects a split first and last name or sending an ISO timestamp to a custom input field expecting MM/DD/YYYY format.

WebMCP mandates that every registered tool must define an inputSchema adhering to the standard JSON Schema specification. This schema outlines:

  • The exact properties the tool accepts
  • The explicit primitive data types
  • Enums restricting values to valid states
  • Formats (such as date, email, or URI)
  • Required vs. optional parameters

By validating the agent’s intended arguments against this JSON Schema before executing any code, WebMCP completely mitigates structural hallucinations. The LLM is forced to format its parameters precisely as your application logic requires.

3. Shared execution context & visual state: unlike server-to-server API calls, WebMCP tools execute directly inside the active browsing context of the user’s current tab. This is a critical distinction. When an agent calls a WebMCP tool, the tool’s handler runs your local client-side JavaScript.

This means the tool can directly read your application’s in-memory state, manipulate the DOM, update form components, or trigger client-side routers. Because this execution happens visually on screen, the human user retains full visibility. They can watch the application update in real time, preserving the human in the loop trust model without abandoning your application’s carefully crafted UI.

WebMCP vs. Standard MCP vs. Actuation

To fully understand where WebMCP fits into your tech stack, let’s compare it using some critical vectors against traditional UI actuation and standard backend Model Context Protocols.

Feature Traditional UI Actuation Standard MCP (Backend) WebMCP (Browser Native)
Execution Environment External Agent (Headless/Virtual Display) Server-side Node/Go/Python Runtime Client-side Browser Tab Sandbox
Interface Medium DOM Selectors, Coordinates, Screenshots Secure APIs, DB Connections, File Systems JavaScript Event Handlers, HTML Form Fields
User Visibility Hidden from user (unless sharing the screen) Completely invisible backend processing Visibly executed on-screen within active UI
State Access Purely visual / Serialized DOM scraping Backend database or session records Live, in-memory client-side application state
Reliability Low (Vulnerable to layout and styling shifts) High (Direct program-to-program APIs) High (Direct program-to-agent contract)
Implementation Complexity Zero on web app; extremely high on agent High backend setup, authentication, proxying Low-to-moderate frontend progressive enhancement

The Two Implementation Models: Declarative vs. Imperative

The Chrome implementation of the WebMCP specification recognizes that web development scales from simple, static semantic documents to highly interactive, state-driven single-page applications. To accommodate this spectrum, it offers two distinct APIs:

1. The Declarative API: It is designed for fast, low-overhead integration. It allows you to transform standard HTML < form > elements into WebMCP tools purely by adding semantic attributes.

You don’t have to write custom JavaScript registries or manually handle schema extraction. The browser parses the form’s inputs, automatically derives a JSON Schema based on HTML5 validation attributes, and exposes it to the agent layer. When the agent invokes the tool, the browser automatically fills out the form fields and triggers the submit event.

2. The Imperative API: It is the powerhouse built for professional web developers handling complex UIs, client-side routing, state management libraries, and custom component design system libraries.

Using standard JavaScript via navigator.modelContext.registerTool(), you explicitly write code that maps the agent’s intent directly to your internal application logic, bypassing DOM interaction altogether if desired and executing clean state mutations.

Setting Up Your WebMCP Development Environment

Because WebMCP is currently tracking through the standards process and is actively under experimental origin trials in Google Chrome, you must explicitly prepare your development environment to recognize and execute the APIs.

Step 1: Flag Activation

  1. Launch Google Chrome
  2. In the omnibox, navigate to: chrome://flags/#enable-webmcp-testing
  3. Locate the WebMCP Testing flag and toggle its state to Enabled.

gil2

  1. Relaunch the browser.

Step 2: Incorporating the Permissions Policy

WebMCP features a robust, built-in security perimeter gated by a dedicated Permissions Policy called tools. By default, the policy resolves to self. This means that tool discovery and execution are permitted inside top-level browsing contexts and same-origin iframes. It strictly blocks untrusted, cross-origin third-party iframes from registering tools that could hijack an agentic session.

If your architecture explicitly demands hosting a WebMCP-enabled application inside a cross-origin iframe (for instance, an embedded booking widget or a white-labeled payment gateway), you must explicitly allow the context via the allow attribute:

< iframe src=”https://trusted-widget.sparxys.com” allow=”tools”></ iframe>

Step 3: Installing the Inspector Subsystem

To debug and verify your application schemas without needing to integrate a massive external agent orchestration framework, you should utilize the Model Context Tool Inspector Extension available via the Chrome Developer ecosystem.

This tool introduces an inspector panel that accesses navigator.modelContext, instantly listing all registered imperative and declarative tools on the active page. It includes a natural language prompting playground backed by locally or cloud-hosted Gemini models (such as Gemini Nano), letting you input natural text to verify that the agent cleanly extracts arguments and maps them to your schemas.

gil3

Implementing the Imperative API

The Imperative API is where real architectural control lives. If you’re building a single-page application with complex state boundaries, you don’t want the agent touching the DOM at all. You want it to talk directly to your logic. Let’s look at how to register a tool using navigator.modelContext.registerTool by building a small usage example with the imperative API. In this example, we will create a small booking form that includes the destination, travel month, and number of guests.

Step 1: Constructing the Semantic HTML Interface

Create an index.html file.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Hotel Search - Imperative WebMCP</title>
    <link rel="stylesheet" href="booking.css">
</head>
<body>

<div class="booking-form">
    <div class="form-group">
        <label>Destination</label>
        <input type="text" id="dest" value="Orlando">
    </div>
    <div class="form-group">
        <label>Travel Month</label>
        <input type="month" id="dates" value="2026-08">
    </div>
    <div class="form-group">
        <label>Guests</label>
        <input type="number" id="guests" value="5">
    </div>
    <button onclick="executeSearch()">Search</button>
</div>

<div id="results"></div>

<script src="scripts.js"></script>
</body>
</html>

Step 2: Authoring the Imperative Registration Script

Now, let’s craft scripts.js. Pay careful attention to how we construct our JSON schema. We want the agent to pass three strictly typed fields: destination, dates, and guests.

function updateUI(dest, dates, guests) {
    const output = `Searching for ${guests} guests in ${dest} for ${dates}...`;
    const render = () => document.getElementById('results').innerText = output;

    if (document.startViewTransition) {
        document.startViewTransition(render);
    } else {
        render();
    }
}

function executeSearch() {
    const dest = document.getElementById('dest').value;
    const dates = document.getElementById('dates').value;
    const guests = document.getElementById('guests').value;
    updateUI(dest, dates, guests);
}

// WebMCP Imperative Registration
if ('modelContext' in navigator) {
    navigator.modelContext.registerTool({
        name: 'searchHotel',
        description: 'Search for hotel room availability by destination, dates, and guest count.',
        inputSchema: {
            type: 'object',
            properties: {
                destination: { type: 'string' },
                dates: { type: 'string' },
                guests: { type: 'number' }
            },
            required: ['destination', 'dates', 'guests']
        },
        execute: async (params) => {
            updateUI(params.destination, params.dates, params.guests);

            // Update visual inputs to reflect agent's actions
            document.getElementById('dest').value = params.destination;
            document.getElementById('dates').value = params.dates;
            document.getElementById('guests').value = params.guests;

            return JSON.stringify({ status: 'success', summary: `Search initiated for ${params.destination}` });
        }
    });
}

Implementing the Declarative API

The imperative API provides complete program control, but it requires you to manually maintain schema layouts in synchronization with your state architecture. For standard text entry pipelines, data processing forms, or support desks, the declarative API is faster and cleaner.

Let’s see how we can change the previous imperative example to use the declarative way.

Writing the Annotated Declarative Markup

By using standard HTML attributes and applying the webmcp-tool configuration layout, we instruct the Chrome parsing layout to handle the registration on our behalf.

Create a separate index file called declarative.html:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Hotel Search - Declarative WebMCP</title>
    <link rel="stylesheet" href="booking.css">
</head>
<body>

<form class="booking-form"
      id="searchForm"
      toolname="searchHotel"
      tooldescription="Search for hotel room availability"
      toolautosubmit>

    <div class="form-group">
        <label for="dest">Destination</label>
        <input type="text" name="destination" id="dest" value="Orlando"
               toolparamdescription="The city to search for hotels in">
    </div>
    <div class="form-group">
        <label for="dates">Travel Month</label>
        <input type="month" name="dates" id="dates" value="2026-08"
               toolparamdescription="The month and year of travel">
    </div>
    <div class="form-group">
        <label for="guests">Guests</label>
        <input type="number" name="guests" id="guests" value="5"
               toolparamdescription="Total number of guests traveling">
    </div>
    <button type="submit">Search</button>
</form>

<div id="results"></div>

<script>
    const form = document.getElementById('searchForm');

    form.addEventListener('submit', (e) => {
        e.preventDefault();
        const data = new FormData(form);
        const outputMsg = `Searching for ${data.get('guests')} guests in ${data.get('destination')} for ${data.get('dates')}...`;

        const updateUI = () => document.getElementById('results').innerText = outputMsg;

        if (document.startViewTransition) {
            document.startViewTransition(updateUI);
        } else {
            updateUI();
        }

        // The agentInvoked is true if the browser's agent triggered the form
        if (e.agentInvoked) {
            e.respondWith(Promise.resolve(JSON.stringify({
                status: 'success',
                message: outputMsg
            })));
        }
    });

    // Listen for agent actuation events for debugging/logging
    window.addEventListener('toolactivated', ({ toolName }) => {
        console.log(`Agent activated WebMCP tool: ${toolName}`);
    });
</script>
</body>
</html>

Deconstructing the Declarative Engine Conversion

What’s elegant here is how much work the browser handles under the hood. By simply using native HTML5 validation attributes and standard markup, WebMCP automatically infers the types, constraints, and enums, exposing a perfectly structured JSON schema to the agent without a single line of manual JavaScript mapping:

  {
    "name": "searchHotel",
    "description": "Search for hotel room availability",
    "inputSchema": {
      "type": "object",
      "properties": {
        "destination": {
          "type": "string",
          "description": "The city to search for hotels in"
        },
        "dates": {
          "type": "string",
          "format": "^[0-9]{4}-(0[1-9]|1[0-2])$",
          "description": "The month and year of travel"
        },
        "guests": {
          "type": "number",
          "multipleOf": 1,
          "description": "Total number of guests traveling"
        }
      },
      "required": []
    }
  }

When the agent attempts to run searchHotel, the browser injects the values safely into the inputs, fires browser-native validation checks to ensure the data matches, and calls the submit event.

Architectural Deep Dive: Security and Data Isolation Boundaries

When discussing protocols that allow external systems to invoke operations within an active web page session, security must be one of our top architectural concerns. If malicious entities could inject tools or call unauthorized actions, WebMCP would represent a massive security vulnerability. The standard addresses this through several core security boundaries:

1. Same-Origin boundary preservation: WebMCP tools are strictly tied to the security context of the origin that registered them. A tool registered by https://secure.sparxys.com cannot be discovered or invoked by an agent looking at a tab pointed to https://attacker-compromised-site.io. The execution boundary honors standard DOM isolation.

2. Mandatory human-in-the-loop patterns: WebMCP doesn’t grant automated agents blind, blanket approval to execute any arbitrary code path in headless background threads. As the browser window must remain active and visible (headless state execution is blocked by the spec design), a user can witness every single manipulation.

For highly sensitive actions such as clicking a final purchase confirmation button, transferring funds, changing root IAM configurations, or modifying passwords, the architecture specification advises embedding standard user confirmation modals inside your JavaScript handler code.

handler: async (args) => {
    const userConsent = await showCustomConfirmationModal("Are you sure you want to authorize this transaction?");
    if (!userConsent) {
        return { success: false, error: "USER DENIED CONSENT" };
    }
    // Proceed with operational routine...
}

This structural safety constraint guarantees that an agent can gather parameters and streamline forms, but the final authorization remains strictly under the user’s control.

Performance Optimization and Memory Management in WebMCP Applications

Exposing structural tools directly inside client-side runtimes requires careful monitoring to avoid memory leaks, sluggish frame rates, and excessive CPU context switching, especially when building on modern SPA stacks like React or Next.js.

1. Avoid stale closure retainers: when registering an imperative tool via navigator.modelContext.registerTool inside a React hook or functional component lifecycle, ensure you clean up properly when components unmount. If you continuously re-register tools on every single re-render cycle, you will accumulate stale closures that retain heavy component state references in memory, leading to memory leaks.

// React useEffect example
useEffect(() => {
    if (!navigator.modelContext || !navigator.modelContext.registerTool) return;

    const abortController = new AbortController();

    navigator.modelContext.registerTool({
        name: "update_dashboard_filter",
        description: "Updates data viewing parameters.",
        inputSchema: { /* ... */ },
        handler: async (args) => {
            // Context execution parameters
        }
    }, { signal: abortController.signal }); 

    return () => {       
        abortController.abort();
    };
}, [reactiveDependencyDependencies]);

2. Keep asynchronous handlers non-blocking: your handler code blocks the agent’s orchestration path until it resolves. If your handler must perform heavy computation, massive client-side data sorting, or wait for sluggish third-party API networks, hand off the tracking workload smoothly. Return a Pending status to the agent or step down execution priorities via requestIdleCallback to prevent locking the browser’s primary UI rendering thread.

Before you continue…

The reading list you'd build – if you had time.

Weekly
Articles + tutorials

The reads you'd find if you had time

2× / mo
Live webinars

Experts you can actually ask

Monthly
Magazine + whitepapers

Deep dives worth your weekend

On-demand
Recordings + courses

Past conferences, ready when you are

Summary

The web is evolving. We are rapidly transitioning away from an era in which web browsers were used exclusively by humans interacting with visual point and click layouts. We are moving into a hybrid web ecosystem where human users are accompanied by highly specialized, context-aware AI agents designed to handle heavy operational lifting.

As senior front-end developers and software architects, our design philosophy must expand. We need to stop thinking about progressive enhancement merely to handle disabled JavaScript or slow mobile connections. We must treat agentic accessibility as a foundational element of the modern front-end engineering stack.

WebMCP gives us the exact blueprint we need to achieve this. By dedicating time to defining clean, strictly typed contracts through imperative registries and declarative HTML5 form layouts, we ensure our applications remain incredibly robust, highly performant, and perfectly discoverable in an increasingly agentic web ecosystem.

The spec is taking shape in real-time. Turn on the flags, test your forms with the inspector extension, and start thinking about your UI as an API.

The post WebMCP: Bridging the Gap Between AI Agents and the Web appeared first on International JavaScript Conference.

]]>
AI, JavaScript, and the End of the Website https://javascript-conference.com/blog/ai-javascript-future-of-web-development/ Mon, 15 Jun 2026 08:57:01 +0000 https://javascript-conference.com/?p=210085 AI is moving rapidly into the web stack. Models run in browsers, agents interact directly with services, and conversational interfaces are challenging traditional navigation. As a result, developers, architects, and UX designers face a broader question: what happens when intelligence becomes a native part of web applications? The answer may change not only how applications are built, but also how users interact with them—and even what we consider a website in the first place.

The post AI, JavaScript, and the End of the Website appeared first on International JavaScript Conference.

]]>
▶ Video Guide: AI, JavaScript, and the End of the Website


Note: This video and podcast was generated using AI, adapting the original content and technical insights created by the author of the iJS blog post.

▶ Podcast Guide: AI, JavaScript, and the End of the Website

Post-AI Shift: What Is Changing?

The foundations of web development remain remarkably stable. Modern web applications are still built with HTML, JavaScript, and CSS. What is changing is not the technology stack itself, but how software is created and who can create it.

Framework expertise remains valuable, but AI increasingly lowers the barrier to entry. Knowledge that once required years of hands-on experience is now embedded in the models developers use every day. With a solid understanding of software engineering principles and effective use of AI tools, developers can work productively across frameworks, languages, and ecosystems that may not be their primary area of expertise.

At the same time, AI is increasing the importance of genuine expertise. Models can generate code, explain APIs, and suggest solutions, but they cannot reliably judge whether a design decision is appropriate, a security risk is acceptable, or an architectural trade-off makes sense in a particular context. The more capable AI becomes, the more valuable expert judgment becomes as the mechanism that validates, challenges, and guides its output.

How AI Is Reshaping Fullstack JavaScript

Fullstack as a discipline won’t go away. It’s gaining an additional dimension. Frontend, backend, and AI each have their own focus, but borders between expertise areas are getting blurrier. Frontend engineers can now deploy applications more easily, implement backend APIs, and cross into areas they previously couldn’t.

If an app’s only server dependency is AI inference, that inference could now move to the frontend, potentially eliminating the server entirely in some cases. Small, focused apps that previously needed a backend just to proxy an API call can now run the whole thing client-side. That’s a genuine architectural change, not just a blurring of roles. But the backend model overall will remain. It may mediate AI inference or connect to a cloud provider.

The question is no longer “are you a frontend or backend developer?” but “where does each piece of logic actually belong, and what’s the most appropriate place to run it?” AI adds a third answer to that question that didn’t exist before.

Learn more about how AI changes software development at iJS New York (September 28 – October 2, 2026):

Before you continue…

The reading list you'd build – if you had time.

Weekly
Articles + tutorials

The reads you'd find if you had time

2× / mo
Live webinars

Experts you can actually ask

Monthly
Magazine + whitepapers

Deep dives worth your weekend

On-demand
Recordings + courses

Past conferences, ready when you are

Does AI Introduce a New Layer of Architectural Complexity in JavaScript?

AI doesn’t alter JavaScript itself, as basic principles stay the same. It changes how systems are extended and orchestrated. Christian Liebel, frontend specialist and frequent iJS speaker, argues that we should think of “AI as an addition to JavaScript, not a transformation of it.”

In the monolithic era, you added code blocks. In the microservices and micro-fronted era, you added services. Now, you add tools and agents. The core idea is similar, but the connectors, dispatching, and routing logic are different.

Sebastian Springer, a React and Node.js expert and regular iJS speaker, argues that AI introduces a new type of architectural component into modern applications. “Usually, you program the decision into your code: is it true or false, is it A, B, or C? Now, you have a smart node in your application, and this makes the decision. So your software becomes much more flexible, much more dynamic.”

Learn more about how AI changes software architecture at iJS New York (September 28 – October 2, 2026):

Where Does AI Inference Fit in Server-Side JavaScript?

Fundamentally, calling an AI is just another HTTP call to an external service. It’s another tool and another abstraction layer. What’s different now is that instead of structured data, you exchange naturally formulated prompts and then have to force the output back into structure to make it machine-processable. However, the challenge consists of enforcing reliable structured output from inherently probabilistic systems.

Streaming APIs are critical, Liebel adds, and many developers are not yet comfortable building backends around them. Node.js now has the Web Streaming API implemented. It allows sharing streaming code between client and server, which is a significant practical improvement.

AI in the Browser

AI in the browser isn’t just the big hype of generative AI. It includes traditional machine learning. Background blurring in Google Meet, for instance, is a long-established example of an AI model running locally. The main question here is, “where is the AI model executed?” There are two approaches:

Approach 1: Bring your own AI

The website brings its own pre-trained model (e.g., from Hugging Face open-weights models) and runs it inside the JavaScript engine already present in the browser. Relevant APIs include WebGPU and WebNN. On the frameworks side of things, you can use Apache TVM, Transformers.js, or ONNX Runtime Web. 

Approach 2: Built-in AI

The browser itself holds the model and runs it. The developer picks the use case, not the model. This is currently implemented in Chrome and Edge. Chrome uses Gemini Nano, and it’s downloaded into the browser and executed on the device. The prompt API is the language model interface for this approach.

Tradeoffs between the two approaches

Bring your own AI Built-in AI
  • Precise model selection, but it has a storage problem.
  • Same-origin policy means models can’t be shared between websites. If every site brings a 5GB model, storage fills up fast.
  • Can use full native performance by running on bare metal rather than through abstraction layers.
  • WebGPU/WebNN are 10-15% slower by comparison, but, in this case, the developer doesn’t know which model they get. Quality can vary unpredictably.

 

“Running AI locally is sort of edge AI, meaning on the user’s device itself. Nobody else can see it. It’s offline-capable and very good for privacy,” says Christian Liebel.

When Local AI Beats the Cloud

There are some cases where local/in-browser AI makes more sense than cloud inference. Liebel identifies the following four points:

  • Cost: No cloud inference costs. This is relevant for companies and hobbyists who want AI features but can’t afford per-user inference fees.
  • Latency: Example: background blurring. Sending video frames to a server and back would be too slow. Local processing is near zero latency.
  • Privacy: Prompt and input data never leave the device.
  • Offline capability: Works regardless of connection state.

Security and Privacy in AI-Powered Web Apps

We need to make a distinction between two security dimensions: AI-assisted attacks, when attackers use AI to find vulnerabilities, and AI-introduced vulnerabilities that emanate from integrating AI into applications.

AI models are good at pattern detection and can find vulnerabilities much more effectively than humans. The errors aren’t necessarily more numerous, they’re just more detectable. Anthropic’s Mythos project is a striking example of this. It uncovered a bug in BSD, a widely used operating system, that had gone undetected by humans for roughly 30 years. This is a reminder of how fragile the IT world actually is and how much AI can see what we can’t.

A practical countermeasure for AI-assisted attacks, suggested by Springer, is to make AI-based security analysis a standard part of your software development process. Use the tools your attackers would use before they can use them against you. AI-assisted vulnerability scanning should be part of your pipeline.

These security concerns exist because AI is increasingly becoming part of the application itself. That raises another question: if AI becomes a native building block of web applications, does it also change what an application looks like?

Before you continue…

The reading list you'd build – if you had time.

Weekly
Articles + tutorials

The reads you'd find if you had time

2× / mo
Live webinars

Experts you can actually ask

Monthly
Magazine + whitepapers

Deep dives worth your weekend

On-demand
Recordings + courses

Past conferences, ready when you are

From Websites to AI-Native Applications

Applications have traditionally been organized around pages. Users navigate through menus, search results, forms, and workflows to reach the information or functionality they need. AI-powered interfaces challenge that model.

Nir Kaufman, fullstack developer, AI instructor and keynote speaker at iJS, describes a future in which users no longer navigate complete applications. Instead, they interact with an AI assistant that guides them through a conversation while dynamically assembling “the entire website chopped into tiny pieces and streamed to me.” A map appears when location matters, a booking component when a reservation is needed, and a payment widget when it’s time to complete a transaction.

This represents more than a new frontend pattern. It changes the role of the application itself. Rather than guiding users through pages and navigation structures, applications expose capabilities that can be invoked as part of an ongoing conversation. The website becomes less of a destination and more of a collection of services, data, and interaction components that an AI assistant can draw upon when needed.

For developers, Kaufman believes this may require a different way of constructing applications. Instead of organizing software around pages, routes, and navigation hierarchies, applications may increasingly be built as collections of reusable components that can be selected and assembled dynamically. In this model, developers design capabilities and interactions rather than predefined user journeys.

Emerging approaches such as Google’s Agent-to-UI (A2UI) protocol explore this idea. Agents describe interface elements as structured objects, while clients render the appropriate user interface on demand. Rather than sending complete screens, systems can stream focused interaction components into an ongoing conversation as they become relevant.

Viewed this way, AI is not simply another feature added to existing systems. It becomes a new access layer between users and applications. The architectural challenge shifts from designing pages and user flows to designing capabilities that can be discovered, combined, and orchestrated dynamically.

Learn more about how AI changes web applications at iJS New York (September 28 – October 2, 2026):

Designing AI-Native User Experiences

If AI becomes a new access layer to applications, the next challenge is no longer primarily technical. It is about designing interactions that people can understand, trust, and control.

Christian Kuhn, UX researcher and consultant, approaches this question through a set of six design principles. Rather than treating AI as a feature, he focuses on how people collaborate with intelligent systems. The principles emphasize human agency, empathy, transparency, controllability, and the ability to recover from mistakes.

This perspective starts with a fundamental shift in how interactions are conceived. As Kuhn puts it, “The central unit of interaction is no longer the page but the conversation itself.” Users increasingly express goals, preferences, and constraints, while the system determines how best to support them. The challenge for designers is therefore not only to create interfaces, but also to shape the interaction between human intent and machine capabilities.

Kuhn’s principles provide a practical framework for this challenge:

  • Human First: AI should amplify human capabilities rather than replace human judgment.
  • Empathy First: Systems should understand user context and needs instead of forcing users to adapt to the machine.
  • Automation vs. Augmentation: Not every task should be fully automated. Users often benefit from remaining active participants in the process.
  • Transparency and Confidence: Users need to understand where information comes from and how reliable it is.
  • Control and Editability: AI-generated results should remain editable, reversible, and subject to user control.
  • Mental Models and Graceful Failure: Systems must help users understand how they work and provide clear recovery paths when mistakes occur.

Several of these principles become increasingly important as AI systems move beyond the chat interface. In Kuhn’s view, “Text writing is de facto the most exhausting and worst input mode.” As voice, vision, and contextual awareness continue to mature, conversational experiences are likely to expand far beyond typing into a text box.

For UX designers, this creates a new design discipline. The goal is no longer limited to optimizing screens, navigation paths, or workflows. It increasingly involves designing how people and AI systems cooperate, communicate, and recover when things go wrong.

Learn more about how AI changes user experience at iJS New York (September 28 – October 2, 2026):

Where Is Web Development Heading?

The architecture of web applications is changing. So is the way users interact with them. As AI becomes part of the web stack, questions that once belonged primarily to developers increasingly overlap with concerns traditionally associated with architects, product teams, and UX designers. Decisions about inference, interfaces, workflows, and user guidance can no longer be treated as separate domains.

This makes collaboration more important, not less. Building AI-powered web applications requires technical, architectural, and experiential perspectives to work together much earlier in the process than many teams are accustomed to today.

At the same time, the emergence of AI does not invalidate the foundations of software engineering. If anything, it reinforces them. Clean architectures, clear documentation, reliable automation, comprehensive testing, strong observability, and security that is considered from the beginning rather than added later remain essential. As systems become more dynamic and more complex, these disciplines become increasingly important.

For web developers, architects, and UX designers alike, the challenge is therefore twofold: to remain open to new architectural and interaction models while continuing to apply the engineering principles that have always been necessary to build reliable software.

The post AI, JavaScript, and the End of the Website appeared first on International JavaScript Conference.

]]>
Angular 22: What’s New in the Latest Release https://javascript-conference.com/blog/angular-22-new-features-onpush-resource-api/ Thu, 04 Jun 2026 09:32:02 +0000 https://javascript-conference.com/?p=209949 In this version, the Angular team has prioritized comprehensive optimization: from the new default OnPush strategy and the fully stable Resource API to seamless accessibility integration. The framework is becoming leaner, faster, and significantly more intuitive. If you have been wondering when the time would come to fully embrace 'next-generation Angular,' the moment is now. In this article, we will explore the key changes in version 22 and how they redefine modern frontend development.

The post Angular 22: What’s New in the Latest Release appeared first on International JavaScript Conference.

]]>
The New Standard: OnPush as the Default

In Angular 22, OnPush is no longer just an optional performance optimization. It is now the default for new components. This change goes beyond performance and reflects the direction Angular is taking with signals. This shift didn’t happen overnight. It is the final step of a carefully planned transition.

iJS Newsletter

Join the JavaScript community and keep up with the latest news!

[mc4wp-simple-turnstile]

The Roadmap to the New Default:

  • The Zoneless Foundation: Starting with version 21, the framework enabled zoneless support by default, signaling the end of the zone.js era and the beginning of a lighter, faster runtime.
  • The Deprecation of “Default:” In version 21.2, the old ChangeDetectionStrategy.Default was officially marked as deprecated. It was essentially rebranded as the Eager strategy, a new, more descriptive name that better reflects what this mode actually does in Angular.
  • The Angular 22 Milestone: Now, in version 22, Angular comes full circle. When you create a new component, OnPush is applied automatically. What used to be a performance-oriented choice is now the default way Angular components work.

What does this mean for your existing projects?

If you’re upgrading an existing codebase, you don’t need to worry about a “breaking change” headache. The Angular team has built an intelligent migration tool into the ng update process that handles the transition automatically.

change detection

When you run the update, the migrator automatically scans your components. Any component that relied on the old, implicit Default behavior is updated to ChangeDetectionStrategy.Eager (Listing 1). This ensures your application behaves exactly as it did before, with no hidden changes to the logic.

You are not forced into OnPush for your entire codebase. The migration preserves your existing behavior while bringing your project in line with the new Angular standards.

Listing 1

@Component({
  selector: 'app-captain-dashboard',
  imports: [
    CrewWidgetComponent,
  ],
  templateUrl: './captain-dashboard.component.html',
  changeDetection: ChangeDetectionStrategy.Eager,
  styleUrl: './captain-dashboard.component.scss',
})
export class CaptainDashboardComponent {

The Stable Milestone: Resource API, Signal Forms & Angular Aria

Angular 22 marks an important step in the maturity of several newer APIs. Resource API, Signal Forms, and Angular Aria are now stable, making them much easier to adopt in production.

  • Resource API has come a long way. Introduced in Angular 19 as an experimental feature, it quickly drew a lot of attention from the community. Now, after several iterations and refinements, resource, httpResource, and rxResource are finally stable.
  • Signal Forms are also stable now, bringing a more modern approach to handling form states. Angular 22 also includes more detailed documentation and improved compatibility with Angular Material and Angular Aria. One nice improvement worth mentioning is the new debounce option for validateAsync and validateHttp. Previously, debouncing had to be applied at the control level, so it affected all validators attached to that control, including synchronous ones like required. That delayed instant feedback for simple checks just to accommodate async calls.
  • Angular Aria is now stable as well, which is especially important for teams building accessible custom components. It gives developers a more reliable foundation for production use.

iJS Newsletter

Join the JavaScript community and keep up with the latest news!

[mc4wp-simple-turnstile]

Simplifying Dependency Injection: The @Service Decorator

Angular is making dependency injection a bit more straightforward. The new @Service() decorator is intended to cover most of the cases where we previously used @Injectable({ providedIn: ‘root’ }), giving us a cleaner and simpler way to define services.
Before Angular 22, the CLI generated services with @Injectable({ providedIn: ‘root’ }), as shown in Listing 2. In Angular 22, the same command now uses the new @Service() decorator by default (Listing 3), keeping the same root-provided behavior but with a shorter, more focused syntax.

Listing 2

@Injectable({
  providedIn: 'root'
})
export class CatService {

Listing 3

@Service()
export class CatService {

Why the switch?

The biggest win here is getting rid of boilerplate. @Injectable is very flexible, but it exposes options like useValue, useClass, or useExisting that most simple services never need.

@Service is much more opinionated. It is aimed at the common case of a singleton service provided in the root injector. By limiting the configuration surface (you only get a simple factory function if you really need it), it helps keep service definitions straightforward.

On top of that, @Service is designed to be used with the inject() function instead of constructor injection, which keeps dependencies closer to where they are used and makes the class easier to read at a glance. If you try to use constructor injection with @Service, Angular will now fail the build with an error.

Listing 4

@Service()
export class CatService {
  
    constructor(http: HttpClient) {
    }
}

The image displays a code snippet error message in a software development environment, specifically related to a TypeScript file in an Angular project, indicating an issue with a service class's constructor dependency injection

Turning off automatic provisioning

By default, @Service() registers the class in the root injector. In some cases, though, you may want the service to exist only within a smaller part of the UI. That is where autoProvided: false comes in (Listing 5). It lets you skip automatic provisioning and add the service manually where you need it.

For example, if a service should live only as long as a specific component is active, you can provide it in that component’s providers array (Listing 6).

Listing 5

@Service({ autoProvided: false })
export class DraftWorkspaceService {
 
}

Listing 6

@Component({
  selector: 'app-draft-editor',
  providers: [DraftWorkspaceService],
  template: `...`,
})
export class DraftEditorComponent {
  protected draftWorkspace = inject(DraftWorkspaceService);
}

If you forget to add the service to the component’s providers array, Angular will fail to resolve it and throw an error, as shown in the screenshot below.

Build error shown after using constructor injection with @Service()

When should you stick with @Injectable?

Use @Injectable() when:

  • Your service requires more advanced provider configuration
  • You need constructor injection
  • The service should live in a non-root scope, such as providedIn: ‘platform’.

In practice, @Service() becomes the new default for simple, root-scoped services, while @Injectable() remains the better tool for advanced DI scenarios. The split is quite clean: use @Service() for the common, simple cases, and switch to @Injectable() when the service needs more flexibility.

Lazy-loaded services with injectAsync()

While @Service() simplifies the common case, Angular now goes one step further with injectAsync(), which is especially useful for lazy-loaded services. Angular has always had a strong dependency injection system, but loading services only on demand has often required more manual work. InjectAsync() makes that much easier by letting Angular resolve a service asynchronously, exactly when it becomes necessary.

This is especially useful when a service depends on a heavy library or supports a feature that is only used occasionally, such as a rich text editor, because you want to keep the initial page load as light as possible. When the service is first requested, the bundler loads a separate JavaScript chunk, and Angular then resolves the service like any other singleton.

Important: Lazy loading works only with auto-provided services, such as @Injectable({ providedIn: ‘root’ }) or @Service().

The example below (Listing 7) shows a dashboard component that loads its chart service asynchronously when it is actually needed. The service is fetched only when showCharts() is called, and only the first call triggers the actual download.

Listing 7

@Component({
  selector: 'app-dashboard',
  template: `...`,
})
export class DashboardComponent {
  private charts = injectAsync(() =>
    import('./dashboard-charts.service').then((m) => m.DashboardChartsService)
  );


  async showCharts() {
    const charts = await this.charts();
    charts.render();
  }
}

If you want to start the download earlier, you can pass a prefetch trigger in the options. By default, it is onIdle (Listing 8), a built-in trigger that waits until the browser becomes idle before starting the load. You are not limited to onIdle, a prefetch trigger can be any function that returns a promise.

Listing 8

export class DashboardComponent {
  private charts = injectAsync(() =>
    import('./dashboard-charts.service').then((m) =>m.DashboardChartsService),
	{ prefetch: onIdle }
  );

Browser URL Support for RouterLinks

Angular 22 introduces a small but useful routing improvement. RouterLink now gets a new browserUrl input. It allows you to set a different URL in the browser than the one Angular uses internally for navigation.

This gives you more control over what users see in the address bar. The app can keep its internal route logic unchanged, while the visible URL feels more user-friendly.

Why it matters:

  • Cleaner and more readable URLs.
  • A visible URL that can differ from the internal route.
  • Better support for aliases or alternative navigation paths.
  • More flexibility in how navigation is presented to users.

This small API addition gives developers more flexibility when shaping how routes appear to users.

debounced() for Signals

Another interesting addition is debounced(), currently available as an experimental feature. It brings a built-in debounce mechanism directly to signals, filling a noticeable gap in signal-based code.

The Problem It Solves

This is especially relevant in UI patterns like autocomplete or search inputs. When an input changes on every keystroke, you usually do not want to trigger an API call immediately each time. Without debouncing, that can easily lead to too many requests while the user is still typing.

Until now, the usual workaround involved a multi-step loop: converting the signal to an Observable, applying RxJS debounceTime, and then converting the result back into a signal. While it worked, it added unnecessary complexity to something that should feel native.

Powered by the Resource API

What makes debounced() so useful is that it gives you both the debounced value and its state in one place. Instead of returning a plain signal, it returns a Resource, allowing you to track exactly what is happening under the hood:

  • loading state: while the timer is running, the resource stays in a loading state and continues to expose the previously settled value.
  • resolved state: once the specified delay passes, the resource automatically moves to resolved with the new value.
  • errors: if the source signal throws an error, the resource switches to an error state immediately.

Automatic cleanup

Because debounced() runs inside an injection context, Angular handles cleanup automatically. When the injector is destroyed, Angular clears any pending timer and disposes of the resource without any extra code.

Example

In practice, this fits naturally into features like user search or autocomplete.

Listing 9

export class UserSearch {
  userQuery = signal('');
  debouncedUserQuery = debounced(this.userQuery, 400);


  results = httpResource<User[]>(() => {
    const search = this.debouncedUserQuery.value();
    return `http://localhost:3000/api/users?query=${search}`;
  });
}

Here, userQuery updates immediately with every keystroke while debouncedUserQuery settles only after 400ms of inactivity. As a result, httpResource does not fire a new request on every single character.

iJS Newsletter

Join the JavaScript community and keep up with the latest news!

[mc4wp-simple-turnstile]

Template comments

Angular now supports both single-line and block comments in templates, which makes it much easier to leave notes right where they matter. That kind of support may sound minor, but it removes one of those tiny everyday annoyances that developers have simply learned to live with. And when a framework takes care of little things like this, the whole developer experience feels smoother.

The example below shows both single-line and block comments in an Angular template.

Listing 10

<div 
  //Single line comment
  class="card">
  
  <h2>User profile</h2>
  <button  
    /* Multi-line 
      comment
      continues here */
    type="button">Edit</button>
</div>

WebMCP

Angular’s latest updates are not only about improving the framework’s core developer experience and performance. The framework is also starting to explore how modern web applications can better integrate with AI-powered workflows.

Web applications are becoming increasingly AI-aware, but communication between AI agents and web apps is still limited. Most agents still rely on DOM inspection and inferred behavior, which makes the whole process fragile and harder to trust.

This is the problem WebMCP aims to solve. Introduced by the Chrome team at Google, WebMCP proposes a more structured way for AI agents to communicate with web applications by exposing actions and workflows directly in the browser. Instead of forcing agents to interpret the entire UI, applications can explicitly define what actions are available and how they should be executed.

While the proposal is still experimental, Angular has already introduced early support for WebMCP, making it easier to integrate AI-friendly capabilities directly into Angular applications.

Currently, Angular provides several integration levels for WebMCP:

  • Global Availability with Application Scope:

When you want to expose features that should be accessible regardless of where the user is currently navigating, you use the application scope. By registering tools using provideExperimentalWebMcpTools within your app.config.ts (Listing 11), you make those capabilities available to AI agents across the entire lifecycle of the application.

A key advantage here is that the execution of these tools runs within an injection context. This means that inside the execute function, you can directly use inject() to access your services.

Listing 11

export const appConfig: ApplicationConfig = {
  providers: [
    provideExperimentalWebMcpTools([
      {
        name: 'searchAdoptionPets',
        description: 'Search the pet adoption registry for animals ready for adoption based on species, age group, and behavioral traits.',
        inputSchema: {
          type: 'object',
          properties: {
            query: { type: 'string', description: 'Search keywords.' },
            species: { type: 'string', enum: ['Dog', 'Cat', 'Rabbit'], description: 'The type of animal.' },
            ageGroup: { type: 'string', enum: ['Puppy/Kitten', 'Young', 'Adult', 'Senior'], description: 'The age group of the animal.' },
          },
          required: ['query', 'species'],
          additionalProperties: false,
        },
        execute: ({ query, species, ageGroup }) => {
          const animals = inject(AnimalsService).search(query, species, ageGroup);
          return { content: [{ type: 'text', text: JSON.stringify(animals) }] };
        },
      },
    ]),
  ]
};
  • Route Scope:

You can also attach WebMCP tools to specific routes by providing provideExperimentalWebMcpTools directly within your route configuration (Listing 12).

However, to ensure this integration behaves predictably, you should configure the router to use withExperimentalAutoCleanupInjectors (Listing 13). This is the key that enables Angular’s automatic cleanup mechanism. When the user navigates away from a route, the framework immediately unregisters the associated AI tools.

Listing 12

export const routes: Routes = [
    {
        path: 'shelter-management',
        loadComponent: () => import('./pages/management/management.component').then((m) => m.ManagementComponent),
        providers: [
            provideExperimentalWebMcpTools([
                {
                    name: 'calculateWeeklySupplyNeeds',
                    description: 'Calculates weekly food and medical supply requirements for a specific shelter zone based on current animal occupancy.',
                    inputSchema: {
                        type: 'object',
                        properties: {
                            sectionName: { 
                                type: 'string', 
                                enum: ['Dog Quarantine', 'Cat Pavilion', 'Small Mammals'], 
                                description: 'The target shelter zone.' 
                            },
                            includeBufferStock: { 
                                type: 'boolean', 
                                description: 'Set to true if the user asks for an extra safety margin or buffer for new arrivals.', 
                                default: false 
                            }
                        },
                        required: ['sectionName'],
                        additionalProperties: false,
                    },
                    execute: ({ sectionName, includeBufferStock }) => {
                        const inventoryService = inject(ShelterInventoryService);
                        const report = inventoryService.calculateSupplies(sectionName, includeBufferStock);
                        return { content: [{ type: 'text', text: JSON.stringify(report) }] };
                    },
                },
            ]),
        ],
    },
];

Listing 13

import { provideRouter, withExperimentalAutoCleanupInjectors } from '@angular/router';


export const appConfig: ApplicationConfig = {
  providers: [
    provideRouter(routes, withExperimentalAutoCleanupInjectors()),
  ]
};
  • Service Scope:

You can also bind WebMCP tools directly to individual services using declareExperimentalWebMcpTool. The tool’s lifecycle is tied strictly to the service instance: it becomes available when the service is initialized and is automatically unregistered when the service is destroyed.

Listing 14

@Service()
export class AdoptionApplicationService {
  readonly activeApplication = signal<AdoptionApplication | null>(null);
  
  constructor() {
    declareExperimentalWebMcpTool({
        name: 'getAdoptionApplicationStatus',
        description: 'Returns the current evaluation status and pending tasks for the active pet adoption application.',
        inputSchema: { type: 'object', properties: {} },
        execute: () => ({
            content: [{ type: 'text', text: JSON.stringify(this.activeApplication()) }],
        }),
    });
  }
}

  • Signal Forms Scope:

This scope bridges the gap between AI assistants and user input by turning standard Angular Signal Forms into intelligent, AI-ready endpoints. Once you register the feature globally via provideExperimentalWebMcpForms (Listing 15), enabling it is as simple as adding the experimentalWebMcpTool property directly to your form definition.

Listing 15

import { provideExperimentalWebMcpForms } from '@angular/forms/signals';


export const appConfig: ApplicationConfig = {
  providers: [
    provideExperimentalWebMcpForms()
  ]
};

The biggest advantage of this scope is zero-boilerplate schema generation. Angular inspects your form’s data model and active validators to dynamically present a structured schema to the browser’s AI. The agent can then fill out and submit the entire form programmatically, handling validation errors directly just like a human user would.

Listing 16

readonly volunteerForm = form(
    this.model,
    (f) => {
      required(f.fullName);
      required(f.email);
      required(f.experience);
      minLength(f.experience, 30);
    },
    {
      experimentalWebMcpTool: {
        name: 'submitVolunteerApplication',
        description: 'Submits an application to become a shelter volunteer. Requires fullName, email, and an experience description of at least 30 characters.',
      },
      submission: {
        action: async (value) => this.volunteerService.registerPendingVolunteer(value),
      },
    },
  );
}

Important: Both WebMCP and Angular’s integration with it are still in an early phase, so some APIs and behaviors may change in future updates.

iJS Newsletter

Join the JavaScript community and keep up with the latest news!

[mc4wp-simple-turnstile]

Conclusion

Angular 22 brings a strong mix of stabilization and new ideas, giving developers both a more solid foundation and a few genuinely interesting directions to explore. The official stabilization of the Resource API, Signal Forms, and Angular Aria makes the framework feel more production-ready in the areas that matter most. At the same time, Angular keeps adding features that solve real-world problems more directly, from routing tweaks to signal-based debouncing and new dependency injection capabilities. Of course, these are only some of the highlights. Angular 22 also includes bug fixes and other improvements that help round out the release.

What stands out most is the direction of these changes. Angular is continuing to reduce friction, replace workarounds with native solutions, and make the developer experience feel smoother without losing the framework’s maturity. It is a release that feels less like a reinvention and more like a confident step forward.

Angular keeps proving that maturity does not have to mean stagnation. What are you most excited to try first in Angular 22?

The post Angular 22: What’s New in the Latest Release appeared first on International JavaScript Conference.

]]>
Watch Session: An AI Assistant for Your Angular Applications https://javascript-conference.com/blog/watch-session-an-ai-assistant-for-your-angular-applications/ Wed, 13 May 2026 11:18:36 +0000 https://javascript-conference.com/?p=209923 AI assistants are becoming an important part of modern web applications. They can help users complete tasks, answer questions, navigate workflows, and interact with application features more naturally. In this session, you’ll learn how Angular applications can use AI assistants to create smarter and more dynamic user experiences.

The post Watch Session: An AI Assistant for Your Angular Applications appeared first on International JavaScript Conference.

]]>

What Is an AI Assistant in Angular?

An AI assistant in an Angular application is more than a simple chatbot. It can understand application context, guide users, and interact with frontend services.

  • It can help users complete tasks inside the application.
  • It can use application state, forms, and routes as context.
  • It can support smarter and more personalized user experiences.

What Is Agentic UI?

Agentic UI allows AI assistants to take action inside the user interface. Instead of only replying with text, the assistant can suggest actions, generate interface elements, and help users move through workflows.

  • AI can become part of the frontend experience.
  • Users can interact with applications in a more natural way.
  • Interfaces can become more dynamic and context-aware.

Using Angular Context

Angular applications already manage a lot of useful context. This includes state, routing, forms, services, and user interactions. When this context is made available to an AI assistant, the assistant can provide better and more relevant support.

  • State management helps the assistant understand current data.
  • Routing helps the assistant understand where the user is.
  • Forms allow the assistant to support input and workflow completion.

iJS Newsletter

Join the JavaScript community and keep up with the latest news!

[mc4wp-simple-turnstile]

Tool-Calling with Angular Services

Tool-calling allows an AI assistant to interact with application functions and services. In Angular, this can be used to connect the assistant with backend APIs, business logic, or frontend actions.

  • Angular services can expose useful functions to the assistant.
  • AI can help trigger actions inside the application.
  • Tool-calling makes the assistant more practical and interactive.

Dynamic UI Generation

One of the most powerful ideas in Agentic UI is dynamic interface generation. AI assistants can help create or suggest UI elements based on user needs, current context, or application data.

  • Interfaces can adapt to the user’s goal.
  • AI can suggest relevant next steps.
  • Applications can become more flexible and intelligent.

Conclusion

AI assistants are changing the way users interact with web applications. For Angular developers, this creates new opportunities to build smarter interfaces that understand context, support workflows, and provide meaningful assistance.

By combining Angular, application state, routing, services, and Agentic UI concepts, developers can create applications that go beyond traditional frontend experiences.

Watch the full session below:

The post Watch Session: An AI Assistant for Your Angular Applications appeared first on International JavaScript Conference.

]]>
Tool Calling in the Frontend with Hashbrown https://javascript-conference.com/blog/tool-calling-frontend-hashbrown-angular/ Thu, 23 Apr 2026 08:36:03 +0000 https://javascript-conference.com/?p=209872 Hashbrown streamlines the complexity of integrating AI assistants into web apps for Angular and other frontend frameworks. Learn how to implement tool calling in the frontend, connect to providers like OpenAI and Google, and securely route requests through a lightweight backend.

The post Tool Calling in the Frontend with Hashbrown appeared first on International JavaScript Conference.

]]>
AI-based assistants improve the user experience and reduce support costs. But implementing them involves a lot of routine technical work, like connecting different LLMs and implementing tool calling. Hashbrown takes this work off our hands. The open-source project, supported by two well-known figures in the Angular community, supports all relevant model providers such as Gemini (Google), GPT (OpenAI), Azure (Microsoft), and Llama (Meta).

This article shows how to extend an existing Angular application with Hashbrown to include a chat assistant. The source code for the demo app is available on GitHub.

Sample application

This sample application is the flight search, which I’ll use to demonstrate several Angular features. Figure 1 shows the chat window that can be displayed on the right-hand side with an example chat history.

Fig. 1: Example application

Fig. 1: Example application

As this chat history shows, the assistant can request additional data and trigger actions in the application as needed. This is made possible by tool calling: the LLM prompts the app to perform a specific function and return the results. These tool calls appear in the chat history as requested by the LLM, with the parameters { from: ‘Graz’, to: ‘Hamburg’ } for findFlights omitted.

While chat messages such as Tool Call: findFlights inform developers about internal processes, this information is likely to be confusing for end users. So it makes sense to translate this technical information into something like Load flights from Graz to Hamburg.

iJS Newsletter

Join the JavaScript community and keep up with the latest news!

[mc4wp-simple-turnstile]

Setting up Hashbrown

To use Hashbrown, we need a few npm packages:

npm install @hashbrownai/{core,angular,google}

The @hashbrown/angular package includes an Angular-based API for the core framework-agnostic library. A framework binding for React is also currently available. The @hashbrown/google package provides access to Google’s Gemini models. Hashbrown offers additional packages for other model families (such as @hashbrown/openai).

For programmatic access to LLMs, the application needs an API key, which is usually linked to a paid license. However, Google’s Gemini has a comprehensive free package for testing purposes. An API key can be generated in Google AI Studio with just a few clicks. The required menu item is available in the dashboard.

To prevent the API key from being published, it must not be used directly in the Angular front end. Instead, use a very narrow back end that serves as an intermediary between the front end and LLM (Listing 1).

Listing 1

// Taken from hasbrown.dev

import express from 'express';
import cors from 'cors';
import { Chat } from '@hashbrownai/core';
import { HashbrownGoogle } from '@hashbrownai/google';

const host = process.env['HOST'] ?? 'localhost';
const port = process.env['PORT'] ? Number(process.env['PORT']) : 3000;

const GOOGLE_API_KEY = process.env['GOOGLE_API_KEY'];
if (!GOOGLE_API_KEY) {
  throw new Error('GOOGLE_API_KEY is not set');
}

const app = express();

app.use(cors());
app.use(express.json());

app.post('/api/chat', async (req, res) => {
  const completionParams = req.body as Chat.Api.CompletionCreateParams;

  const response = HashbrownGoogle.stream.text({
    apiKey: GOOGLE_API_KEY,
    request: completionParams,
    transformRequestOptions: (options) => {

      options.model = 'gemini-2.5-flash';

      options.config = options.config || {};
      options.config.systemInstruction = `
      You are Flight42, an UI assistent that helps passengers with finding flights.

      - Voice: clear, helpful, and respectful.
      - Audience: passengers who want to find flights or have questions about booked flights.
      
      Rules:
      - Only search for flights via the configured tools
      - Never use additional web resources for answering requests
      - Do not propose search filters that are not covered by the provided tools
      - Do not propose any further actions
      - Provide enumerations as markdown lists
      `;

      return options;
    },
  });

  res.header('Content-Type', 'application/octet-stream');

  for await (const chunk of response) {
    res.write(chunk);
  }

  res.end();
});

app.listen(port, host, () => {
  console.log(`[ ready ] http://${host}:${port}`);
});

The implementation of this backend, which was taken in part from the Hashbrown documentation, expects the API key to be stored in the GOOGLE_API_KEY environment variable. On MacOS and Linux, this can be done with:

export GOOGLE_API_KEY=abcde...

And on Windows with:

set GOOGLE_API_KEY=abcde...

With transformRequestOptions, the backend can supplement or override the options set by the frontend—an important mechanism, as these settings have direct cost implications. In the example, the backend enforces the inexpensive all-round model gemini-2.5-flash and defines a system instruction that strictly limits the model to flight searches. This prevents users from consuming expensive LLM resources for unrelated queries.

Before overwriting, the frontend’s original values are stored in model and systemInstructions. This allows controlled negotiation. At the user’s request, the server can switch to a more powerful (but more expensive) model in selected cases or adjust the system instructions.

When the Angular application is started, this minimal server’s URL is configured via provideHashbrown (Listing 2).

Listing 2

import { provideHashbrown } from '@hashbrownai/angular';
[…]

bootstrapApplication(AppComponent, {
  providers: [
    provideHttpClient(),

    […]
    
    provideHashbrown({
      baseUrl: 'http://localhost:3000/api/chat',
      middleware: [
        (req) => {
          console.log('[Hashbrown Request]', req);
          return req;
        }
      ]
    }),
  ],
});

The optional middleware specified here logs all requests to the server on the JavaScript console. These messages give us a better understanding of how such systems work and also help with troubleshooting.

Chatting with the AI of your choice

Hashbrown provides several implementations of Angular’s Resource API for chatting with the LLM. For our purposes, we’ll use chatResource (Listing 3).

Listing 3

@Component({ … })
export class AssistantChatComponent {

  […]
  message = signal('');

  chat = chatResource({
    model: 'gemini-2.5-flash',
    system: `
      You are Flight42, an UI assistant that helps passengers with finding flights.
      […]
    `,
    tools: [
      findFlightsTool,
      toggleFlightSelection,
      showBookedFlights,
      getBookedFlights,
      […]
    ],
  });

  submit() {
    const message = this.message();
    this.message.set('');
    this.chat.sendMessage({ role: 'user', content: message });
  }

  […]
}

The chatResource provides the stateless LLM with the complete chat history for each request. This allows the model to refer to previous statements. For example, if a conversation revolves around flight #4711, the LLM recognizes what is meant by “this flight.”

The chatResource also supports tool calling. The tools property provides the model with all the functionalities provided by the front end, like findFlights for flight searches. The technical implementation of the tools can be found below. The value of the chatResource contains the chat history to be displayed (Listing 4).

Listing 4

@for (message of chat.value(); track $index) {
<article class="msg assistant">
  <div class="avatar">{{ icons[message.role] }}</div>
  <div>
    <div class="bubble">
      {{ message.content }} 
      
      @if (message.role === 'assistant') { 
        @for(toolCall of message.toolCalls; track toolCall.toolCallId) {
          <div [title]="toolCall.args | json">
            Tool Call: {{ toolCall.name }}
          </div>
        } 
      }
    </div>
  </div>
</article>
}

The role property specifies who sent the chat message. For example, the value assistant indicates messages from the LLM, while user indicates messages from the front-end user. Messages from the LLM can also contain requests for tool calls, which are also presented in the template shown. Each tool call refers to the name of the desired tool (e.g., findFlights) and the arguments to be passed (e.g., { from: ‘Graz’, to: ‘Hamburg’ }).

iJS Newsletter

Join the JavaScript community and keep up with the latest news!

[mc4wp-simple-turnstile]

Providing tools

The tools provided are objects that the application creates with the createTool function (Listing 5).

Listing 5

import { createTool } from '@hashbrownai/angular';
import { s } from '@hashbrownai/core';
[…]

export const findFlightsTool = createTool({
  name: 'findFlights',
  description: `
  Searches for flights and redirects the user to the result page where the found flights are shown. 
  
  Remarks:
  - For the search parameters, airport codes are NOT used but the city name. First letter in upper case.
  `,
  schema: s.object('search parameters for flights', {
    from: s.string('airport of departure'),
    to: s.string('airport of destination'),
  }),
  handler: async (input) => {
    const store = inject(FlightBookingStore);
    const router = inject(Router);

    store.updateFilter({
      from: input.from,
      to: input.to,
    });

    router.navigate(['/flight-booking/flight-search']);
  },
});

The tool name must be unique and comply with the model specifications. Here’s a practical rule of thumb: anything that is permitted as a variable name in TypeScript should also work here. The LLM uses the description to decide if the tool is relevant for the current task. The schema defines the arguments that the model must pass—in the example, an object with the search parameters from and to. Here the LLM is guided by the stored textual descriptions.

Hashbrown uses its own schema language, Skillet, to define this structure. It’s similar to the Zod library, but is reduced to constructs that reliably support LLMs. Future Hashbrown versions will also support JSON Schema and bridging to Zod.

The handler implements the tool: it receives the object defined in the schema and delegates the task to the system logic, such as the store or the router. Handlers can also return values to the model, like the getLoadedFlights tool (Listing 6).

Listing 6

export const getLoadedFlights = createTool({
  name: 'getLoadedFlights',
  description: `Returns the currently loaded/ displayed flights`,
  handler: () => {
    const store = inject(FlightBookingStore);
    return Promise.resolve(store.flightsValue());
  },
});

The return value is not formally described in Skillet—the model accepts any form of response. If the front end wants to provide the model with information about the structure of the delivered result, this can be done as free text in the description field.

iJS Newsletter

Join the JavaScript community and keep up with the latest news!

[mc4wp-simple-turnstile]

Under the hood

A look at the messages sent to the LLM shows how tool calling works (Listing 7):

  • Hashbrown sends the user’s text search query in the user role to the model.
  • The model responds in the assistant role with a tool call. This includes the name of the tool and the arguments to be passed.
  • Hashbrown triggers the tool, which handles the search and route change.
  • Hashbrown reports back in the tool role that the tool call has been completed. If the tool had returned a result, Hashbrown would include it in this message.
  • The model responds in the assistant role.

To ensure that the LLM is aware of the tools offered, these are transferred together with metadata in the tools section at the end. Here, you’ll find textural descriptions stored in the source code and the schema definitions of expected arguments.

Listing 7

{
  "model": "gpt-5-chat-latest",
  "system": "You are Flight42, an UI assistant [...]",
  "messages": [
    [...],
    {
      "role": "user",
      "content": "Ok, let's search for flights from Graz to Hamburg."
    },
    {
      "role": "assistant",
      "content": "",
      "toolCalls": [
        {
          "id": "call_AeFJ3xsnNw29EoQVo7hR9Qtu",
          "index": 0,
          "type": "function",
          "function": {
            "name": "findFlights",
            "arguments": "{\"from\":\"Graz\",\"to\":\"Hamburg\"}"
          }
        }
      ]
    },
    {
      "role": "tool",
      "content": {
        "status": "fulfilled"
      },
      "toolCallId": "call_AeFJ3xsnNw29EoQVo7hR9Qtu",
      "toolName": "findFlights"
    },
    {
      "role": "assistant",
      "content": "Here are the available flights [...]",
      "toolCalls": []
    }
  ],
  "tools": [
    {
      "description": "Searches for flights [...]",
      "name": "findFlights",
      "parameters": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "type": "object",
        "properties": {
          "from": {
            "type": "string",
            "description": "airport of departure"
          },
          "to": {
            "type": "string",
            "description": "airport of destination"
          }
        },
        "required": [
          "from",
          "to"
        ],
        "additionalProperties": false,
        "description": "search parameters for flights"
      }
    },
    [...]
  ]
}

For a better visualization, Figure 2 shows the process as a sequence diagram. This diagram also shows the backend, which allows the frontend to access the model.

Fig. 2: Message history with Tool Calling

Fig. 2: Message history with Tool Calling

Conclusion

Hashbrown makes it easy to add chat-based AI assistants to front-end applications. It handles complex tasks like LLM integration and tool calling, allowing developers to focus on actual business benefits. In just a few steps, you can create an assistant that controls user interactions, triggers application functions, and responds contextually.

In practice, it’s important to note that LLMs do not work deterministically—the same query can lead to slightly different results. It’s also worth refining tool descriptions step-by-step and testing them with typical sample queries to achieve reliable interaction between the model and the application.

The post Tool Calling in the Frontend with Hashbrown appeared first on International JavaScript Conference.

]]>
If You Want to Understand Modern Angular with Signals, You Have to Relearn Angular https://javascript-conference.com/blog/modern-angular-signals-agentic-ui/ Tue, 31 Mar 2026 11:53:48 +0000 https://javascript-conference.com/?p=209838 Angular has undergone a fundamental transformation—not a gradual one, but a complete overhaul. The reactivity model is new, the testing framework is new, and at the forefront of this evolution is Agentic UI: the ability to integrate language models directly into the application architecture. This is more than just an API update.

The post If You Want to Understand Modern Angular with Signals, You Have to Relearn Angular appeared first on International JavaScript Conference.

]]>
How SignalStore, Agentic UI, and modern testing fit together

I learned more about this in a conversation with Manfred Steyer. Manfred has been training enterprise teams in Angular for years and is the instructor for our Modern Angular Masterclass. His assessment: Many teams adopt modern Angular without changing their underlying mindset. They’re just writing old Angular code using new syntax.

Signals are not an API update—they require a shift in thinking

Anyone who treats Signals as if they were just a better EventEmitter hasn’t yet fully embraced the real change in Angular.

To explain this, Manfred uses a catchy metaphor: Most developers think of their application as a script—do this, then that, and output this data. But Angular has stopped expecting scripts. Signals require data-flow-oriented thinking—inputs lead to data, data leads to more data, and the end result is what the user sees. The metaphor is more like a river, not a script.

Anyone who sees this merely as new syntax for building the same things as before remains stuck in the old paradigm and misses the point of modern Angular.

Ten years of Angular experience can actually be a hindrance today

Anyone who has been writing Angular for ten years has built up ten years’ worth of patterns—but tragically, some of them are now getting in the way.

Anyone learning Modern Angular with old patterns is setting themselves up for failure—that’s how Manfred explains it. Karma, Jasmin, imperative workarounds that were once necessary are no longer in the projects; they’re in people’s minds. Modern Angular deliberately leaves this legacy behind, and that requires an active decision, not just regarding the tools but also regarding the mindset.

iJS Newsletter

Join the JavaScript community and keep up with the latest news!

[mc4wp-simple-turnstile]

Testing was broken. Everyone knew it, but hardly anyone said it out loud

If you wanted to test Angular components properly, you had to know when Angular runs its change detection internally. That’s not a testing task—it’s forensic analysis.

With Vitest and Browser Mode, that’s changing. The component becomes a black box: you test what it exposes to the outside world, not how it works internally. Manfred calls this “component testing” to distinguish it from “unit testing”—a level more abstract, closer to what users actually experience. Karma and Jasmin are deprecated, and migration is due anyway—what results when you do it right is more than just a tool switch.

Store and Forms: reactivity as a guiding principle

Anyone who views SignalStore merely as a replacement for NgRx has yet to grasp its most important feature.

Manfred describes it as the headless version of the application—a complete cross-section of the application logic, detached from any UI layer. This is an architectural decision, far more than just an implementation detail. And then there’s Signal Forms. According to Manfred, it is “perhaps the most beautiful API Angular has ever produced”—the third forms API, built on years of grappling with the pain points of the first two. Forms are no longer treated as a special case of reactive data flow, but as a consistent part of it. Store and Forms together show just how far Angular has taken reactive logic.

When the LLM controls the application, the architecture determines the outcome

Agentic UI isn’t a future scenario; it’s an architectural decision that teams need to make right now.

Interestingly, this is where the SignalStore comes back into play. Manfred describes a scenario that many recognize as a vision: an AI sidecar that pops up when needed. The user chats, and the sidecar controls the application via client-side tool calling, connected to the store—routes, forms, and data. The Store, as a headless variant, is precisely the link that underpins this architecture.

What Manfred doesn’t leave out—and what I consider the most important part: What happens if the model does something unintended? Human-in-the-loop, validation mechanisms, scorers—you can’t do without them. These are design decisions that must be finalized before the first production rollout.

iJS Newsletter

Join the JavaScript community and keep up with the latest news!

[mc4wp-simple-turnstile]

You don’t change paradigms on a whim

The path to modern Angular doesn’t lie in a list of APIs; it lies in a shift in mindset and, with it, a new way of working. Anyone who skips this step will, sooner or later, hit a wall—at the latest when a language model takes remote control of their application.

In his Angular workshop at iJS Conference London, Manfred brings you up to speed with where Angular is headed and shows you how to use it in practice. You’ll learn how to design a clean, reactive architecture with Signals and build lean state management using the new NgRx Signal Store. But it doesn’t stop there: You’ll also create an AI-powered assistant that understands your app, guides users, and generates dynamic UI. Check out the full conference program and workshop details here.

The post If You Want to Understand Modern Angular with Signals, You Have to Relearn Angular appeared first on International JavaScript Conference.

]]>
Remodel your TypeScript Code with Decorators https://javascript-conference.com/blog/remodel-typescript-code-with-decorators/ Wed, 25 Mar 2026 14:42:43 +0000 https://javascript-conference.com/?p=209817 Learn how to clean up your TypeScript code by declaring behaviors orthogonally, by walking through a series of before-and-after code examples where we use Decorators to reduce code weight and improve readability.

The post Remodel your TypeScript Code with Decorators appeared first on International JavaScript Conference.

]]>

TypeScript has a fantastic mechanism called Decorators for attaching behaviors in an orthogonal way. It’s most commonly used by framework developers, but application developers have a lot to gain from using them as well.

The purpose of the decorator is to attach useful functionality to our code in a declarative way that clearly communicates what’s going on without cluttering up the code.

Angular developers will recognize such decorators as @Component, which identifies a class as a Component, @Injectable, which registers a class with the Angular dependency injection engine, and @ViewChild, which provides a code reference to an element in the HTML view.

Note that TypeScript Decorators look syntactically similar to Annotations you may be familiar with from languages such as Java. The big difference is that Annotations are compile-time modifiers that provide metadata to the element they modify, while Decorators are run-time functions that can also wrap or transform existing code.

Use Cases

Rather than starting with the boring details of how to create decorators, let’s dig straight into some interesting use-cases where decorators can help you clean up your code and provide valuable functionality with minimal effort.

iJS Newsletter

Join the JavaScript community and keep up with the latest news!

[mc4wp-simple-turnstile]

Memoize

Memoization is a common code optimization technique that automatically caches results for every set of input parameters to a function for reuse. This works in situations where the same inputs will always produce the same output (stable), and the output is the only goal (no side effects or operations). A memoized function returns the cached result for every call after the first. This can be used for intensive calculations (the Fibonacci sequence is a common example), or expensive calls such as to a database or API.

Implementing memoization on a function can be just a bit messy and makes the original intent of the function slightly less clear. Below are two examples of memoization, contrasted against the extremely simple original functions.

// Database Example
class UserService {
  getUser(userId: string): User {
    return this.db.fetchUser(userId)
  }

  memoizedGetUser(userId: string, memo?: {[key: string]: User}): User {
    memo = memo ?? {}
    if (!memo[userId]) memo[userId] = this.db.fetchUser(userId)
    return memo[userId]
  }
}
const userService = new UserService()
const user = memoizedGetUser('XY43797')

// Fibonacci Example
class MathUtils {
  fibonacci(n: number): number {
    if(n < 0) throw new Error('Positive numbers only please')
    return n < 2 ? n : ( fibonacci(n - 1) + fibonacci(n - 2) )
  }

  memoizedFibonacci(n: number, memo?: {[key: number]: number}): number {
    if(n < 0) throw new Error('Positive numbers only please')
    memo = memo ?? {}
    if(memo[n]) return memo[n]
    return memo[n] = n < 2 ? n : (
      fibonacci(n - 1,  memo) + fibonacci(n - 2, memo)
    )
  }
}
const mathUtils = new MathUtils()
console.log('fibonacci', 5, mathUtils.memoizedFibonacci(5))
console.log('fibonacci', 12, mathUtils.memoizedFibonacci(12))

Using a @Memoize decorator, we can accomplish the same functionality with none of the complexity visible in our code.

@Memoize
getUser(userId: string): User {
  return this.db.fetchUser(userId)
}

@Memoize
fibonacci(n: number): number {
  if(n < 0) throw new Error('Positive numbers only please')
  return n < 2 ? n : ( fibonacci(n - 1) + fibonacci(n - 2) )
}

All of the complexity has been abstracted away into the implementation of the decorator. Before we get into these implementation details, let’s look at a few more examples.

Measure Performance

It can often be useful to measure the time it takes for a method to complete. This can help us find bugs, performance issues, or room for improvement through techniques such as Memoization.

The following example measures the total time taken to perform a function call and logs the results using Console Timers.

class UserService {
  fetchUser(email: string): Promise<User> {
    return this.db.query('Users', 'email', email)
  }

  fetchUserMeasureTime(email: string): Promise<User> {
    console.time('UserService#fetchUser') // Needs to be unique
    const result = db.query('Users', 'email', email)
    console.timeEnd('UserService#fetchUser')
    return result
  }
}

While sometimes we may want to leave this performance measuring code in production, other times we just want to be able to quickly add and remove it during testing. This is tricky when we may have to change the way the function returns, such as in the example above. Additionally, we must be careful to always use a unique value for the timer label to avoid errors or inaccurate results. We can handle these considerations with a @PerfLog decorator, which can be easily added anytime and is easily discoverable.

class UserService {
  @PerfLog
  fetchUser(email: string): Promise<User> {
    return this.db.query('Users', 'email', email)
  }
}

We can go even further and add configurable performance monitoring to an entire class. The Angular framework has a series of lifecycle hooks that application developers can use to respond to setup, update, and tear down events. Through a carefully crafted @AngularPerformance() class decorator, we can automatically add performance markers for these events to the browser’s performance data along with measurements of the component startup time.

import { Component } from '@angular/core';
import { AngularPerformance } from './angular-performance.decorator';
import { environment } from '../environments/environment.development';

@AngularPerformance(!environment.production)
@Component({
  selector: 'app-root',
  template: '<h1>{{title}}</h1>',
})
export class ExampleComponent {
  title = 'angular';
}

With just a single line of code (plus imports), we’ve added performance monitoring to all Angular lifecycle events for the class for all non-production environments, providing a tremendous amount of visibility into application behavior.

When doing a performance recording in Chrome, the lifecycle marks show up in the timing diagram and in the event listing as shown below.

Screenshot of the Performance tab in the Google Chrome developer tools, showing the Gantt chart of event timings above and a list of timing events in a list below

Figure 1: Screenshot of the Performance tab in the Google Chrome developer tools, showing the Gantt chart of event timings above and a list of timing events in a list below

Parameter Management

It’s very common for us to perform standard operations on function parameters, including and especially making sure that we handle missing or null parameters appropriately. Two common tasks include returning null if the parameters are missing and logging the values sent to a function for troubleshooting purposes. Without decorators, we could do this.

class RandomStuff {
  // Log parameter names & values
  sendMessage(fullName: string, email: string): Promise<boolean> {
    console.debug('RandomStuff#sendMessage', fullName, email)
    // Do stuff here
    return true
  }

  // If the parameter is null, return null
  function getUser(userId: string): User {
    if(userId === null) {
      return null
    }
    return this.db.fetchUser(userId)
  }
}

This isn’t hard to do, but it clutters up the code with things that aren’t directly related. It’s also a bit harder to make this configurable at the application level or to find the places we are (or should be) applying this behavior. We can accomplish the same thing with @LogParams(level) and @PassNull(match) method decorators, which accept configuration parameters to control behavior.

class RandomStuff {
  @LogParams('debug')
  sendMessage(fullName: string, email: string): Promise<boolean> {
    // Do stuff here
    return true
  }

  @PassNull()
  function getUser(userId: string): User {
    return this.db.fetchUser(userId)
  }
}

This allows our functions to maintain their cohesion while still adding the intended functionality.

Persistence

While memoization is great for functions that are called repeatedly while an application is running, sometimes we need persistence between application runs. One common use case for this is to maintain user preferences on their device, or to hold state in the browser (outside of the session) in case the user reloads the page. Without persistence, we might see this:

class UserService {
  public userId: string
}

const userService = new UserService()
userService.userId = 'XY43797'
console.log('User ID', userService.userId)

In this example, the userId property is publicly readable and writable, but I have no way to capture changes to this property to add persistence. Fortunately, TypeScript has us covered with Accessors, which are get and set methods for class properties.

class UserService {
  private _userId: string

  get userId() {
    return this._userId ?? (
      this._userId = localStorage.getItem('UserService_userId')
    )
  }

  set userId(id: string) { 
    localStorage.setItem('UserService_userId', this._userId = id)   
  }
}

const userService = new UserService()
userService.userId = 'XY43797'
console.log('User ID', userService.userId)

In the above example, you’ll notice that we read/write to the property the same way we did before, but behind the scenes, the get and set accessor functions are being called. This has allowed us to keep a localStorage property in sync with the class property _userId so that this value will be available between sessions. We can further simplify this example with the use of an accessor decorator.

class UserService {
  private _userId: string

  @Persist
  get userId() { return this._userId }
  set userId(id: string) { this._userId = id }
}

There are two possible surprises about the accessor decorator. The first is that we have to decorate an accessor instead of just decorating the property itself. The reason for this is that property decorators (which do exist) are unable to add behaviors or attach code. The second surprise is that we have just one decorator instead of decorating both get and set separately. The reason is that accessors are treated as a single unit, and you’ll actually get an error if you attempt to add the decorator to both accessors. You can decorate either get or set as you wish, and these functions don’t have to be adjacent in the code, although you’ll find your code much easier to read if you keep them together.

iJS Newsletter

Join the JavaScript community and keep up with the latest news!

[mc4wp-simple-turnstile]

Decorator Implementation

Now that we’ve seen some compelling examples of custom decorators, it’s time to show off the implementation. Note that we’re only using three types of decorators in this article: Method decorators, Class decorators, and Accessor decorators. TypeScript additionally supports Property and Parameter decorators.

Getting Started

Before you can jump right into the code, it’s important to note that Decorators are still currently an experimental feature that you must enable in your TypeScript configuration file.

{
  "compilerOptions": {
    "lib": ["es6"],
    "module": "commonjs",
    "experimentalDecorators": true,
    "target": "es6",
  }
}

The important entry in this example tsconfig.json file is the experimentalDecorators setting, which must be true for your decorators to work. It’s also important that the target be set to ECMAScript 6 or later, as shown here.

@Memoize

We’ll start by showing the implementation of the method decorator for memoize.

export function Memoize(
  target: any,
  methodName: string,
  descriptor: PropertyDescriptor
) {
  // function details
}

This is the standard signature for a method decorator function. The parameters are:

  • target – the object instance of the class containing the decorated method. This allows you to modify the underlying object
  • methodName – the name of the decorated method (for example, “getUser” or “fibonacci”)
  • descriptor – the metadata of the method being modified

Because this is a method decorator, the actual method we’re decorating can be found in descriptor.value, which should be treated as a function reference. We’ll replace this function with our own implementation so that we can modify it, but we have to make sure to call the original function, so the underlying functionality is unaltered.

const originalMethod = descriptor.value

  descriptor.value = function(...args: any[]) {
    originalMethod.apply(this, args)
  }

The next step is to set up our memo and define the key we’ll use to uniquely identify values in the memo based on the parameter values. It’s important that we account for multiple parameters.

const memo: { [key: string]: any } = {}
  descriptor.value = function(...args: any[]) {
    const _key = [target.constructor.name, methodName, ...args]
      .map(o => o.toString()).join('_')
  }

The memo has a string key and any type of object value, and the key is defined as a concatenation of the class name, the method name, and the argument values. The last step is to read from the memo whenever possible and update the memo with the value when necessary. This gives the following final implementation.

export function Memoize(
  target: any,
  methodName: string,
  descriptor: PropertyDescriptor
) {
  const memo: { [key: string]: any } = {}
  const originalMethod = descriptor.value
  descriptor.value = function(...args: any[]) {
    const _key = [target.constructor.name, methodName, ...args]
      .map(o => o.toString()).join('_')
    return memo[_key] ?? ( memo[_key] = originalMethod.apply(this, args) )
  }
}

PropertyDescriptor

Don’t let the type name PropertyDescriptor throw you off. Technically, all members of a class are “properties,” whether they are simple values, objects, functions, or accessors, so you’ll see this descriptor across all of our example decorators. The properties of the descriptor are all optional, and include:

  • configurable – a boolean value that indicates if the descriptor itself can be changed, such as changing writable or enumerable properties, or if the property can be deleted from its containing object.
  • enumerable – indicates if this property will be included in iterations over the object, such as for…in loops or Object.keys().
  • value – the actual value of the property, which may be data or a function.
  • writable – a boolean value that indicates if the property can be changed. Setting this to false will cause future reassignment attempts to be ignored in non-strict mode or to throw an error in strict mode.
  • get – the actual property getter function for accessor descriptors. When the property is accessed, this function is called, and its return value becomes the property’s value.
  • set – the actual property setter function for accessor descriptors. When the property is assigned a new value, this function is called with the new value as an argument.

@PerfLog

Similar to the Memoize decorator, @PerfLog is a simple method descriptor that we’ll use to inject timing calls before and after the original function does its thing.

const _context = `${target.constructor.name}_${methodName}`

  const originalMethod = descriptor.value

  descriptor.value = function(...args: any[]) {
    const _key = `${_context}_${globalThis.performance.now()}`
    console.time(_key)
    const retVal = originalMethod.apply(this, ...args)
    console.timeEnd(_key)
    return retVal
  }

We start by setting context, which includes the class name and method name, because console timers require unique values. We then account for multiple calls to the same function by adding a high-performance timer value to the key we use for the timer to ensure that every single call to the decorated function will have its own unique timer. Otherwise, we’re overriding the original function, adding a timer call before and after the method, and then returning any value from the original method.

This works well in many cases, but most of the time we want to time a function, there will be a Promise involved. We don’t actually want to measure how long it takes to return a Promise, but how long it takes for the promise to complete once all the work is done. This requires some extra code.

 if(retVal && typeof (retVal as PromiseLike<any>).then === 'function') {
      return (retVal as PromiseLike<any>).then(value => {
        console.timeEnd(_key)
        return value
      })
    }

This does a type check against the return value to see if it’s a promise that we need to wait for, in which case we call the end timer once the promise is complete. Putting this all together gives the final implementation for @PerfLog.

export function PerfLog(
  target: any,
  methodName: string,
  descriptor: PropertyDescriptor
) {
  const _context = `${target.constructor.name}_${methodName}`
  const originalMethod = descriptor.value

  descriptor.value = function(...args: any[]) {
    const _key = `${_context}_${globalThis.performance.now()}`
    console.time(_key)
    const retVal = originalMethod.apply(this, ...args)
    if(retVal && typeof (retVal as PromiseLike<any>).then === 'function') {
      return (retVal as PromiseLike<any>).then(value => {
        console.timeEnd(_key)
        return value
      })
    }
    console.timeEnd(_key)
    return retVal
  }
}

@LogParams(level)

Unlike our previous decorators, we want to provide configuration to the @LogParams decorator to indicate the logging level (debug, info, etc.) that we want to use for reporting the parameters and values. This will require us to use a Decorator Factory.

export function LogParams(
  level: 'debug' | 'info' | 'warn' | 'error' = 'debug'
): MethodDecorator {
  return (
    target: any,
    methodName: string,
    descriptor: PropertyDescriptor
  ) => {
    // implementation here
  }
}

The decorator factory takes a single optional parameter of level, which we default to “debug”, making this the value if the decorator is used without a parameter, such as @LogParams(). Note that the use of parentheses is not optional for decorator factories. The factory returns a MethodDecorator which has the same function signature we’re familiar with.

The logging of parameters itself is very straightforward, and we use the provided level as a parameter to the console. Note that we’re handling the case where no arguments are provided to the function call.

if(args.length) console[level](`${methodName} Params`, ...args)
      else console[level](`${methodName} Params void`)

@PassNull(match)

We’ll also implement @PassNull as a decorator factory so that we can accept a match parameter of “any” or “all”. This specifies if we want to return null automatically when any one of the method parameters is null, or only if every one of the method parameters is null.

export function PassNull(match: 'any' | 'all' = 'any'): MethodDecorator {
  return (
    target: any,
    methodName: string,
    descriptor: TypedPropertyDescriptor<any>
  ) => {
    const originalMethod = descriptor.value

    descriptor.value = function(...args: any[]) {
      switch(match) {
        case 'all':
          if(args?.length && args.every(arg => arg === null)) return null
          break
        case 'any':
          if(args?.length && args.some(arg => arg === null)) return null
          break
      }

      return originalMethod.apply(this, args)
    }
  }
}

Let’s look at how this behaves in an example context.

class UserService {
  @PassNull('all')
  findUser(id: string, email: string) {
    // do stuff
  }

  @PassNull() // any
  changeEmail(oldEmail: string, newEmail: string) {
    // do stuff
  }
}

const userService = new UserService()

userService.findUser(null, '[email protected]') // ok
userService.findUser('XY43797', null)          // ok
userService.findUser(null, null)               // null

userService.changeEmail('[email protected]', '[email protected]') // ok
userService.changeEmail(null, [email protected]')                // null
userService.changeEmail('[email protected]', null)                  // null

@Persist

As explained above, we’ll implement persistence with an Accessor Decorator using localStorage. While local storage is a Web API, there are implementations available for Node.js that would allow this to run across environments. We’ll get right to it, since no factory is needed and the method signature looks familiar.

export function Persist(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
  const _key = `${target.constructor.name}/Persist/${propertyKey}`

  const originalGetter = descriptor.get
  descriptor.get = function () {
    const prop = originalGetter.call(this)
    return originalGetter.call(this) ?? globalThis.localStorage.getItem(_key)
  }

  const originalSetter = descriptor.set
  descriptor.set = function(value: any) {
    globalThis.localStorage.setItem(_key, value)
    originalSetter.call(this, value)
  }
}

We’re using globalThis, so the example code can be run and tested in a Node.js environment, but in a web environment, this will be effectively synonymous with window. As we’ve done with method decorators, we’re wrapping the existing functionality, but rather than descriptor.value we’re using .get and .set to obtain the accessor functions.

iJS Newsletter

Join the JavaScript community and keep up with the latest news!

[mc4wp-simple-turnstile]

@AngularPerformance(enabled)

Before digging into the promise of tracking performance of Angular lifecycle events, let’s take a look at what those events are:

  • constructor – when Angular instantiates the component
  • ngOnInit – once all inputs have been initialized
  • ngAfterContentInit – after the component’s content has been initialized
  • ngAfterViewInit – after the component’s view has been initialized
  • ngOnChanges – every time inputs have changed
  • ngDoCheck – every time this component is checked for changes
  • ngAfterContentChecked – every time the content has been checked for changes
  • ngAfterViewChecked – every time the view has been checked for changes

There are additional events for rendering and destruction of the component, but we’ll only be tracking the above events for this example. Read more about these lifecycle events here.

To do everything we’ve promised, we’ll start with a Class Decorator Factory and our own custom interface for convenience.

export function AngularPerformance(enabled: boolean = true) {
  return function _ClassDecorator<T extends NgClassConstructor>(target: T) {
    if(enabled) {
      // setup event hooks
    }
    return target
  }
}

interface NgClassConstructor {
  new(...args: any[]): {}
}

The enabled parameter defaults to true, allowing us to apply this decorator as simply @AngularPerformance(), but this allows us to conditionally disable this decorator.

Unlike the @PerfLog decorator, here we’ll be using the Performance API instead of the Console API. The requirements are similar: provide a unique name with every call. We can use mark for point-in-time snapshots and measure to calculate a range of time between marks. Because setting up the tracking will be the same for every event hook, and because it’s quite tedious, we’ll abstract this into its own function.

function setupMethod(constructor: any, methodName: string) {
  const className = constructor.name
  const original = constructor.prototype[methodName]
  let index = 0

  constructor.prototype[methodName] = function(...args: any) {
    const _key = `${className}_${methodName}_${index++}`
    globalThis.performance.mark(_key)
    if(original) {
      original.apply(this, args)
    }
  }
}

Conveniently, the lifecycle hooks are void functions, so we don’t need to worry about return values. Note that we’re building a unique key using the class name, method name, and an incrementing index, so each call gets its own mark.

NOTE: this code never cleans up after itself by calling clearMarks, so it can absolutely cause memory leaks and as-is should never be left running for extended periods of time.

We can use this to finish setting up event hooks.

      setupMethod(target, 'ngOnInit')
      setupMethod(target, 'ngAfterContentInit')
      setupMethod(target, 'ngAfterViewInit')
      setupMethod(target, 'ngOnChanges')
      setupMethod(target, 'ngDoCheck')
      setupMethod(target, 'ngAfterContentChecked')
      setupMethod(target, 'ngAfterViewChecked')

This would be good enough for many uses, but we want to additionally measure various startup times for the component.

  • init: component → ngOnInit
  • contentInit: ngOnInit → afterContentInit
  • viewInit: ngOnInit → afterViewInit

These measurements will show up in the browser performance graph and will help us understand the startup timing of the component. Unfortunately, this introduces a new wrinkle because we need consistent names for the marks we’re using to measure between. We’ll start by setting up the model and configuration.

export function AngularPerformance(enabled: boolean = true) {
  return function _ClassDecorator<T extends NgClassConstructor>(target: T) {
    if(enabled) {
      setupMethod(target, 'ngOnInit',
        { measurementName: 'init', markStart: 'constructor' })
      setupMethod(target, 'ngAfterContentInit',
        { measurementName: 'contentInit', markStart: 'ngOnInit' })
      setupMethod(target, 'ngAfterViewInit',
        { measurementName: 'viewInit', markStart: 'ngOnInit' })
      // no change to other event hooks
      globalThis.performance.mark(`${target.name}_constructor`)
    }
    return target
  }
}

type Measure = {
  measurementName: string
  markStart: string
}

function setupMethod(
  constructor: any,
  methodName: string,
  measurement?: Measure
) {
  constructor.prototype[methodName] = function(...args: any) {
    if(measurement) {
      // Handle measurements
    }
    // original code
  }
}

The new Measure type gives us a parameter to use for specifying what to measure. In addition to specifying the start and end points of the measurement, we need to create a mark for the constructor that we can measure from. In most contexts, we could wrap the constructor function to add this mark at either the beginning or the end of the constructor function, but this is not compatible with Angular due to the dependency injection that takes place. We’ll use the configuration of the decorator itself as a reasonable timing surrogate, as we can reasonably expect this to run just before the component is constructed.

With all of this configuration work done, now we need to perform the measurements.

      const perf = globalThis.performance
      const _start = `${className}_${measurement.markStart}`
      const _end = `${className}_${methodName}`
      perf.mark(_end)
      perf.measure(
        `${className}_${measurement.measurementName}_${index++}`,
        _start,
        _end,
      )

In each case, the end point of the measurement is the function being called, which allows us to specify the endpoint. Technically, marking this is a duplication since we already have another mark at this point, but it’s necessary so that we have a non-indexed mark that can be referenced by the next measurement. With this, we can put it all together for the final @AngularPerformance(enabled) implementation.

export function AngularPerformance(enabled: boolean = true) {
  return function _ClassDecorator<T extends NgClassConstructor>(target: T) {
    if(enabled) {
      setupMethod(target, 'ngOnInit',
        { measurementName: 'init', markStart: 'constructor' })
      setupMethod(target, 'ngAfterContentInit',
        { measurementName: 'contentInit', markStart: 'ngOnInit' })
      setupMethod(target, 'ngAfterViewInit',
        { measurementName: 'viewInit', markStart: 'ngOnInit' })
      setupMethod(target, 'ngOnChanges')
      setupMethod(target, 'ngDoCheck')
      setupMethod(target, 'ngAfterContentChecked')
      setupMethod(target, 'ngAfterViewChecked')
      globalThis.performance.mark(`${target.name}_constructor`)
    }
    return target
  }
}

interface NgClassConstructor {
  new(...args: any[]): {}
}

type Measure = {
  measurementName: string // measurement name
  markStart: string // method name
}

function setupMethod(
  constructor: any,
  methodName: string,
  measurement?: Measure
) {
  const perf = globalThis.performance
  const className = constructor.name
  const original = constructor.prototype[methodName]
  let index = 0

  constructor.prototype[methodName] = function(...args: any) {
    if(measurement) {
      const _start = `${className}_${measurement.markStart}`
      const _end = `${className}_${methodName}`
      perf.mark(_end)
      if(!perf.getEntriesByName(_start, 'mark')?.length){
        console.warn('Missing starting performance mark', _start)
        return
      } else if(!perf.getEntriesByName(_end, 'mark')?.length) {
        console.warn('Missing ending performance mark', _end)
        return
      }
      perf.measure(
        `${className}_${measurement.measurementName}_${index++}`,
        _start,
        _end,
      )
    }
    const _key = `${className}_${methodName}_${index++}`
    perf.mark(_key)
    if(original) {
      original.apply(this, args)
    }
  }
}

It’s important to note that this implementation will track lifecycle events even if we don’t have those events implemented within the Angular class, making it particularly useful compared to the longhand method of implementing this type of timing class-by-class and method-by-method.

iJS Newsletter

Join the JavaScript community and keep up with the latest news!

[mc4wp-simple-turnstile]

Conclusion

We’ve introduced TypeScript decorators, including getting started with instructions and concrete code examples. We’ve explored six unique use cases where decorators can remodel our code for a better living experience, and we’ve shown example code both with and without the decorators for emphasis. Please check out the full source code at GitHub and reach out to me here with any questions and to share your experience implementing these examples. Enjoy decorating!

The post Remodel your TypeScript Code with Decorators appeared first on International JavaScript Conference.

]]>