Description
Summary
We want to enable the capability of interacting with Blazor components from JavaScript as well as support interacting with other javascript frameworks that might be running on the page. To support this, we will enable creating Blazor components from JavaScript and attach those components to specific DOM elements. We will support passing parameters to those components via JavaScript as well as removing those components from the DOM once they are no longer needed.
Motivation
We want to support integrating Blazor with other component frameworks and just be another "JavaScript" framework in general that developers can integrate with their applications. That means developers should be able to have an existing application like an MVC + jquery, or an existing SPA application (like React, Angular, Vue, Svelte, etc.), be capable to integrate Blazor components dynamically whenever they need it and interact with them from within the Browser.
Goals
- Support dynamically rendering components from JavaScript into the DOM, not just during startup.
- Interact with any other JavaScript framework/technology running on the page.
- Enable building
component libraries
that can offer reusable Blazor components for consumption from other applications. - Enable multi-framework teams to leverage Blazor.
Non-goals
TBD
Scenarios
- As a customer I want to render a Blazor component as the result of the user interacting with the page, for example when the user clicks a button.
- I can have a completely static page for the most part, and when I require the user to input some data, I can display a modal dialog with a Blazor powered form handle validation and submision of the form to an API.
- As a customer I want to integrate a Blazor component within my existing application and interact with it from JavaScript.
- I can have an existing SPA application and I want to use a new reporting component to display detailed information about my business. The logic for controling some aspects of the data is handled externally (filters, etc).
- As a customer I want to offer my controls as a reusable library that third party vendors can load on to their page and use.
Risks
- Integration with other frameworks might not be possible/smooth in all cases.
- This is a large area so there are many unknowns that we'll have to deal with as we make progress.
- There are security considerations to be had when using this from Blazor server.
- Type deserialization
- Policy to avoid an unlimited number of components per circuit.
Interaction with other parts of the framework
TBD
Detailed design
This is a very large area, so we will divide the design in phases, each of which will provide additional capabilities and deliver incremental value to customers.
Phase one - Enable interaction with Blazor from JavaScript
This phase covers the minimum work strictly required to unblock customers to interact with Blazor from JavaScript, which then they can build additional experiences on top.
Creating components from JavaScript
We will need to offer new APIs to create components with JavaScript. These APIs will be exposed from the global Blazor object, and will return an object developers can use to further interact with the component.
var component = Blazor.renderComponent(<<some-sort-of-descriptor>>, htmlElement, initialParameters);
Removing/destroying components from JavaScript
We will need to offer some APIs to destroy existing components and free up resources once the components are no longer being used. These APIs can be exposed directly through the proxy returned by the renderComponent method. For example:
component.dispose()
We will name this method dispose since it is a convention known from .NET developers. Disposing a component this way will free up all .NET resources in use by the component as well as offer an opportunity for the component and its descendants to free up all DOM resources/state they were using via the standard dispose mechanism.
Pass parameters to the component
We will need to pass parameters from JavaScript to the component during the initial render as well as during successive renders. In general, there is no strongly typed contract for passing in parameters to a component. Since a component receives a ParameterView
instance which is comparable to a list of IEnumerable<KeyValuePair<string,object>>
where the key is the name of the parameter and the value is represented as an object. However, during deserialization we need to know the types of the parameters or otherwise we won't be able to correctly deserialize them. In the case of server-side Blazor we also want to make sure we don't deserialize any unknown parameters and create the possibility of a type explosion.
Based on the points made above, we need to require some way of registering what parameters a "dynamic" root component is willing to accept. When a component receives parameters from JavaScript there are four cases to consider:
- The parameter has a "simple" type that we can "convert" without loosing precission:
- This normally means primitive types like strings, numbers, booleans, etc.
- Numbers can be generally converted to int/long when they don't have decimals and to doubles when they do; booleans can be converted to true and false; strings don't need conversion.
- "Complex" parameters are those that are represented as JavaScript objects and for which there is no implicit mapping to a C# type (other than JsonElement if we are using STJ for deserialization). In addition to that, "primitive" types are always passed in "by value", while for complex types we need to decide whether they should be passed in by value or by reference. There are several ways to solve this problem:
- Implicit mappings for complex parameters are not supported and we will throw an exception.
- Implicit mappings for complex parameters will be supported as long as those parameters are JsObjectReferences.
- Implicit mappings for complex parameters will have "reference" semantics and will be automatically converted to JsObjectReference instances.
- "Callable" (function) parameters present an additional challenge, since the component that receives them should be able to invoke them, pass arguments to the invocation and receive a result back.
- We currently don't have a way to do this, since there is no type to represent a JavaScript function via JS interop. Based on this there are several options:
- Functions are not supported and developers must write a wrapper object to expose them to their component.
- Functions are represented as JSObjectReference and can be invoked via JS interop (something like
InvokeAsync("", ..args))
. - We extend JS interop with a new type
JSFunction
that represents a JavaScript function that is directly invokablejsFunc.InvokeAsync(...args)
- We currently don't have a way to do this, since there is no type to represent a JavaScript function via JS interop. Based on this there are several options:
- "Registered parameters" offer a solution for many of the questions we described above. By forcing the developer to be explicit about the number and types of parameters its component its willing to accept, we can make better decissions about how to map incoming parameters from JavaScript to C#.
- This doesn't necessarily mean a developer must map all parameters explicitly in code. We can leverage existing attributes like
Parameter, CascadingParameter, etc.
to automatically detect the parameters a component is willing to accept.- Developers can register components that can be created from JavaScript during startup.
- We can scan the component type for
Parameter
andCascadingParameter
attributes. - We will map "primitive" and "complex" parameters to their C# counterpart.
- We will map "function-like" parameters to delegates that will invoke the passed in JS function/object.
- "Function-like" parameter types (Delegates, EventCallback) present special challenges of their own. These types of parameters enable integration from Blazor with the JavaScript world in a way that makes JSInterop "implicit". Given that, they have the same limitations we impose in other JS interop scenarios.
- No sync interop (meaning non-task returning delegates are not supported)
- Same security considerations as other JS interop scenarios in Blazor server.
- We can treat
CascadingParameter
just as a regularParameter
attribute since there is no cascading value provider wrapping it. Its valuable to make it work because that way avoids having to wrap those components just for using them as root components.
- We can allow developers to tweak the registrations after the discovery step we perform to create the initial mapping. This enables developers to retain full control over the JS to .NET mapping that is independent of the base class or attributes Blazor provides.
- This is important since our "opinions" can change over time as we evolve the framework.
- The fact that we register components for "accuracy" doesn't mean we can't still maintain an implicit mapping for unknown parameters.
- Direct parameters are mapped to their explicitly registered types.
- "Primitive" parameters are mapped to their best match.
- "Complex" parameters are mapped to JSObjectReferences
- "Functions" are mapped to "JSObjectReference/JSFunction".
- This doesn't necessarily mean a developer must map all parameters explicitly in code. We can leverage existing attributes like
The gauge here is how much we want to make these types of components work in general situations without requiring explicit changes to the component definition or requiring additional steps to register the component parameters.
Pass information out from the component to JavaScript
There are already several ways to do this, for example doing JS interop from the component. However, is a common pattern that elements/component can communicate with other components via "event-like" interfaces. In this sense there are several options:
- Explicit JS interop via static functions, JSObjectReference, etc.
- Existing "function/event" based types like
Action, Function, EventCallback
. - Custom events (this is specific to HTML elements) which allow an event to be raised through the DOM tree in the same way as a regular event, like a click, change, keydown, etc event.
The first two options are already supported by Blazor, the third option is not, however it is a common way to interact with elements on a page, so it is interesting we consider supporting it.
Expose an "interface" from the component to the "JavaScript" world that can be consumed through "idiomatic" javascript code.
Given that these components will have a higher level of interaction with JavaScript, it might make sense to offer an interface that can naturally be directly consumed via JavaScript. For example, if we are building a "form" component in Blazor, it might make sense that it offers a set of methods to interact with the component through JavaScript, like validate
, reset
, submit
, etc.
We can consider a new feature that components can implement to offer this "stream-lined" interface to the JavaScript world. We can mark the component somehow to "expose" some methods directly on the JS proxy created as a result of rendering a new instance of the component and we can use that gesture to also make the component re-render automatically after one of those methods is called in the same way it happens when a component handles an event.
Going back to the example above, it means that when a component receives a call to validate
, reset
or submit
it can avoid an explicit call to StateHasChanged
.
Content inside existing elements
So far we've described components that don't have "content" inside them. However there can be situations where it is useful to have content within them, for example if the layout for a page/section is done in Blazor and the content is done with a different framework.
Should Blazor offer a way to represent these inner nodes and pass those to the component upon initialization?
An HTML element node could be abstracted into a RenderFragment
and rendered with the same syntax as one, with the exception that the content is already provided by someone else. There would need to be a way to represent these special "markup" primitives in a render batch and additional handling on the browser renderer to find the given html element and insert it in the dom at the right position.
This opens the door to more complex interactions between frameworks than just "islands" within frameworks.
Phase two - Integration with other frameworks
This phase focuses on features and fixes to ensure it is possible to integrate properly with other UI frameworks. We need to establish the requirements we can impose on other frameworks to ensure we can integrate with them.
For example, when we render a component into a DOM element it is fundamental that Blazor has full control over the rendering and updates of that element and that the presence of additional children created by Blazor doesn't interfere with the other framework rendering process as well as updates from the other framework don't interfere with the rendered nodes for the element.
In this sense, any framework that wants to be supported here needs to be able to:
- Treat the element node that will host the Blazor component as an "opaque" container, which means it won't interact with the node contents.
- Retain the node that hosts the Blazor component across UI updates, which means that if an update happens after the Blazor component has been rendered into a node, it won't destroy the existing node and replace with a new instance and instead will just reuse the existing node and just update any attributes if necessary.
- Expose a valid reference to the HTML element after it has been rendered into the DOM so that developers can use Blazor to attach the component to it.
At this stage we will also ensure that Blazor is also able to integrate with popular web standards in this space (web components related standards) like custom elements, shadow DOM or slotted content.
As a result, we should be able to:
- Register a custom HTML element and render a Blazor component within it.
- Render a Blazor component inside a
shadow root
. - Respect
slotted
content inside the element where we are rendering the component.
By supporting the standards associated with web components we can make sure that developers can author a set of components that cleanly integrate with any other framework.
Phase three - Smooth out the experience
This is additional work that we can choose to do if we have time and that will cover the remaining experience gaps around authoring component libraries that can be consumed by other frameworks.
- A potential new template/option to generate a "Blazor web component library" with ready to use components.
- Automatic registration of web components as custom elements, with rendering inside the shadow DOM.
Plan of work
Given the large amount of work in this space we will break down the work into smaller pieces of work that can be implemented in smaller chunks of work.
- Render component from JavaScript in Blazor Webassembly.
- Render component from JavaScript in Blazor Server.
- Render component from JavaScript in Blazor Desktop.
- Remove component from JavaScript in Blazor Webassembly.
- Remove component from JavaScript in Blazor Server.
- Remove component from JavaScript in Blazor Desktop.
- Pass primitive parameters to a component from JavaScript in Blazor Webassembly.
- Pass primitive parameters to a component from JavaScript in Blazor Server.
- Pass primitive parameters to a component from JavaScript in Blazor Desktop.
- Pass complex parameters to a component from JavaScript.
- Pass function parameters to a component from JavaScript.
- Expose events from a component to JavaScript.
- **Expose an interface from a component to JavaScript.
- Can render a component from another SPA framework like React.
- Can pass parameters to a component from another SPA framework like React.
- Can destroy the component when the element is destroyed by another SPA framework like React.
- Supports rendering a Blazor component as a custom element.
- Supports rendering a Blazor component inside a shadow root.
- Supports rendering a Blazor component inside an element with slotted content.