Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 53 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,20 @@

Elsa Studio is a modular, extensible application framework built with MudBlazor and is used to manage Elsa workflows and related entities.

## 🆕 New: Shadow DOM Support

Elsa Studio now supports Shadow DOM for complete style isolation when embedding as custom elements! This prevents style conflicts between host applications and Elsa Studio components.

```html
<!-- Regular custom element (may have style conflicts) -->
<elsa-workflow-definition-editor definition-id="my-workflow"></elsa-workflow-definition-editor>

<!-- Shadow DOM custom element (fully isolated styles) -->
<elsa-workflow-definition-editor-shadow definition-id="my-workflow"></elsa-workflow-definition-editor-shadow>
```

[📚 Learn more about Shadow DOM support](./docs/SHADOW_DOM.md)

## Prerequisites

- **.NET SDK**: Ensure you have both [.NET 7](https://dotnet.microsoft.com/download/dotnet/7.0) and [.NET 8](https://dotnet.microsoft.com/download/dotnet/8.0) SDKs installed.
Expand Down Expand Up @@ -43,6 +57,45 @@ steps but should you run into any issues or want to build these ahead of time, t
1. **For Blazor Server Host**: `dotnet run --project .\src\hosts\Elsa.Studio.Host.Server\Elsa.Studio.Host.Server.csproj --framework net8.0`
1. **For Blazor Server WASM**: `dotnet run --project .\src\hosts\Elsa.Studio.Host.Wasm\Elsa.Studio.Host.Wasm.csproj --framework net8.0`

## Custom Elements & Shadow DOM

The Custom Elements host provides web components that can be embedded in any web application:

1. **For Custom Elements Host**: `dotnet run --project .\src\hosts\Elsa.Studio.Host.CustomElements\Elsa.Studio.Host.CustomElements.csproj --framework net8.0`

### Available Custom Elements

| Component | Regular Element | Shadow DOM Element |
|-----------|-----------------|-------------------|
| Backend Provider | `elsa-backend-provider` | `elsa-backend-provider-shadow` |
| Workflow Definition Editor | `elsa-workflow-definition-editor` | `elsa-workflow-definition-editor-shadow` |
| Workflow Instance Viewer | `elsa-workflow-instance-viewer` | `elsa-workflow-instance-viewer-shadow` |
| Workflow Instance List | `elsa-workflow-instance-list` | `elsa-workflow-instance-list-shadow` |
| Workflow Definition List | `elsa-workflow-definition-list` | `elsa-workflow-definition-list-shadow` |

### Enabling Shadow DOM

Configure Shadow DOM support in `appsettings.json`:

```json
{
"ShadowDOM": {
"Enabled": true
}
}
```

## Framework Integration

Elsa Studio custom elements work seamlessly with modern frameworks:

- **Angular**: Use as regular custom elements in templates
- **React**: Integrate with `useRef` and event handlers
- **Vue.js**: Use with standard Vue component patterns
- **Vanilla JS**: Create and manage dynamically

See the [framework integration examples](./src/hosts/Elsa.Studio.Host.CustomElements/wwwroot/framework-integration-demo.html) for detailed implementation patterns.

Explore the [Elsa Studio GitHub repository](https://github.com/elsa-workflows/elsa-studio) for more detailed information.

## Localization
Expand Down
158 changes: 158 additions & 0 deletions docs/SHADOW_DOM.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
# Shadow DOM Support for Elsa Studio

This document describes the Shadow DOM support implementation in Elsa Studio, which enables full style encapsulation when embedding Elsa Studio components as custom elements.

## Overview

Shadow DOM provides a way to encapsulate styles and markup within a component, preventing style conflicts between the host application and Elsa Studio components. This is particularly useful when embedding Elsa Studio in applications with existing styles that might conflict.

## Benefits

- **Style Encapsulation**: Prevents style bleed from the host application into Elsa Studio and vice versa
- **Safe Embedding**: Enables safer embedding in diverse frontend environments (Angular, Vue, React, etc.)
- **Modern Web Standards**: Aligns with modern Web Components best practices

## Configuration

Shadow DOM support can be enabled via configuration in your `appsettings.json`:

```json
{
"ShadowDOM": {
"Enabled": true
}
}
```

## Usage

### Enabling Shadow DOM Support

In your `Program.cs`, the Shadow DOM feature is automatically configured based on the configuration setting:

```csharp
// Get shadow DOM configuration
var enableShadowDOM = configuration.GetValue<bool>("ShadowDOM:Enabled", false);

if (enableShadowDOM)
{
// Register custom elements with Shadow DOM support
builder.RootComponents.RegisterCustomElementWithShadowDOM<BackendProvider>("elsa-backend-provider-shadow");
builder.RootComponents.RegisterCustomElementWithShadowDOM<WorkflowDefinitionEditorWrapper>("elsa-workflow-definition-editor-shadow");
// ... other components
}
else
{
// Register custom elements without Shadow DOM (original behavior)
builder.RootComponents.RegisterCustomElement<BackendProvider>("elsa-backend-provider");
builder.RootComponents.RegisterCustomElement<WorkflowDefinitionEditorWrapper>("elsa-workflow-definition-editor");
// ... other components
}
```

### Custom Elements

When Shadow DOM is enabled, components are registered with a `-shadow` suffix:

| Regular Custom Element | Shadow DOM Custom Element |
|------------------------|----------------------------|
| `elsa-backend-provider` | `elsa-backend-provider-shadow` |
| `elsa-workflow-definition-editor` | `elsa-workflow-definition-editor-shadow` |
| `elsa-workflow-instance-viewer` | `elsa-workflow-instance-viewer-shadow` |
| `elsa-workflow-instance-list` | `elsa-workflow-instance-list-shadow` |
| `elsa-workflow-definition-list` | `elsa-workflow-definition-list-shadow` |

### HTML Usage

```html
<!-- Without Shadow DOM (styles may conflict) -->
<elsa-workflow-definition-editor definition-id="some-id"></elsa-workflow-definition-editor>

<!-- With Shadow DOM (styles isolated) -->
<elsa-workflow-definition-editor-shadow definition-id="some-id"></elsa-workflow-definition-editor-shadow>
```

## Implementation Details

### TypeScript/JavaScript

The Shadow DOM functionality is implemented in TypeScript modules:

- `shadow-dom.ts`: Core Shadow DOM functions
- Automatic stylesheet injection for Elsa Studio dependencies
- Custom element registration with Shadow DOM support

### C# Interop

The C# side provides:

- `IDomAccessor` interface with Shadow DOM methods
- `DomJsInterop` implementation for JavaScript interop
- `ShadowDOMExtensions` for easy component registration

### Stylesheets

The following stylesheets are automatically injected into Shadow DOM roots:

- `_content/MudBlazor/MudBlazor.min.css`
- `_content/CodeBeam.MudBlazor.Extensions/MudExtensions.min.css`
- `_content/Radzen.Blazor/css/material-base.css`
- `_content/Elsa.Studio.Shell/css/shell.css`
- `_content/Elsa.Studio.Workflows.Designer/designer.css`

## Advanced Usage

### Custom Stylesheet Configuration

You can provide custom stylesheets when setting up Shadow DOM:

```javascript
// JavaScript/TypeScript
setupElsaShadowRoot(element, [
'path/to/custom-styles.css',
'_content/MudBlazor/MudBlazor.min.css',
// ... other stylesheets
]);
```

### Programmatic Shadow DOM Creation

```javascript
// Create a shadow root with Elsa Studio styles
const shadowRoot = setupElsaShadowRoot(element);

// Register a custom element with Shadow DOM
registerBlazorCustomElementWithShadowDOM('my-custom-element', 'MyBlazorComponent');
```

## Browser Support

Shadow DOM is supported in all modern browsers:

- Chrome 53+
- Firefox 63+
- Safari 10+
- Edge 79+

For older browsers, consider using polyfills or falling back to regular custom elements.

## Examples

See `shadow-dom-demo.html` for a complete example demonstrating the difference between regular and Shadow DOM custom elements.

## Troubleshooting

### Common Issues

1. **Styles not loading**: Ensure all required stylesheets are available at the specified paths
2. **Blazor not initializing**: Check that Blazor is loaded before custom elements are used
3. **Component not rendering**: Verify the component is properly registered and the tag name matches

### Debugging

Enable browser developer tools to inspect Shadow DOM elements:

1. Open DevTools
2. Go to Settings (F1)
3. Enable "Show user agent shadow DOM"
4. Inspect the custom element to see its Shadow DOM tree
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from './element';
export * from './element';
export * from './shadow-dom';
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
/**
* Creates a shadow DOM root for the given element
*/
export function createShadowRoot(element: HTMLElement, mode: 'open' | 'closed' = 'open'): ShadowRoot {
if (element.shadowRoot) {
return element.shadowRoot;
}

return element.attachShadow({ mode });
}

/**
* Injects stylesheets into a shadow DOM root
*/
export function injectStylesheets(shadowRoot: ShadowRoot, stylesheets: string[]): void {
stylesheets.forEach(href => {
const link = document.createElement('link');
link.rel = 'stylesheet';
link.href = href;
shadowRoot.appendChild(link);
});
}

/**
* Gets the default Elsa Studio stylesheets
*/
export function getElsaStudioStylesheets(): string[] {
return [
'_content/MudBlazor/MudBlazor.min.css',
'_content/CodeBeam.MudBlazor.Extensions/MudExtensions.min.css',
'_content/Radzen.Blazor/css/material-base.css',
'_content/Elsa.Studio.Shell/css/shell.css',
'_content/Elsa.Studio.Workflows.Designer/designer.css'
];
}

/**
* Sets up a shadow DOM root with Elsa Studio styles
*/
export function setupElsaShadowRoot(element: HTMLElement, customStylesheets?: string[]): ShadowRoot {
const shadowRoot = createShadowRoot(element);
const stylesheets = customStylesheets || getElsaStudioStylesheets();
injectStylesheets(shadowRoot, stylesheets);
return shadowRoot;
}

/**
* Registers a Blazor custom element with Shadow DOM support
*/
export function registerBlazorCustomElementWithShadowDOM(tagName: string, componentName: string): void {
if (customElements.get(tagName)) {
return; // Already defined
}

class ElsaShadowCustomElement extends HTMLElement {
private _elsaShadowRoot: ShadowRoot;

constructor() {
super();
this._elsaShadowRoot = this.attachShadow({ mode: 'open' });
injectStylesheets(this._elsaShadowRoot, getElsaStudioStylesheets());
}

connectedCallback() {
// Wait for Blazor to be ready
if (window.Blazor && window.Blazor.rootComponents) {
window.Blazor.rootComponents.add(this._elsaShadowRoot, componentName, this);
} else {
// Blazor not ready yet, wait for it
document.addEventListener('DOMContentLoaded', () => {
if (window.Blazor && window.Blazor.rootComponents) {
window.Blazor.rootComponents.add(this._elsaShadowRoot, componentName, this);
}
});
}
}
}

customElements.define(tagName, ElsaShadowCustomElement);
}

// Extend the global Window interface to include Blazor
declare global {
interface Window {
Blazor: {
rootComponents: {
add(element: Element | ShadowRoot, componentName: string, parameters?: any): void;
};
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,10 @@ public interface IDomAccessor
Task<DomRect> GetBoundingClientRectAsync(ElementRef elementRef, CancellationToken cancellationToken = default);
Task<double> GetVisibleHeightAsync(ElementRef elementRef, CancellationToken cancellationToken = default);
Task ClickElementAsync(ElementRef elementRef, CancellationToken cancellationToken = default);

// Shadow DOM support
Task CreateShadowRootAsync(ElementRef elementRef, string mode = "open", CancellationToken cancellationToken = default);
Task InjectStylesheetsAsync(ElementRef elementRef, string[] stylesheets, CancellationToken cancellationToken = default);
Task SetupElsaShadowRootAsync(ElementRef elementRef, string[]? customStylesheets = null, CancellationToken cancellationToken = default);
Task CreateElsaCustomElementAsync(string tagName, string componentName, bool enableShadowDOM = false, CancellationToken cancellationToken = default);
}
6 changes: 6 additions & 0 deletions src/framework/Elsa.Studio.DomInterop/Interop/DomJsInterop.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,10 @@ public class DomJsInterop(IJSRuntime jsRuntime) : JsInteropBase(jsRuntime), IDom
public async Task<DomRect> GetBoundingClientRectAsync(ElementRef element, CancellationToken cancellationToken = default) => await InvokeAsync(async module => await module.InvokeAsync<DomRect>("getBoundingClientRect", cancellationToken, element.Match()));
public async Task<double> GetVisibleHeightAsync(ElementRef elementRef, CancellationToken cancellationToken = default) => await InvokeAsync(async module => await module.InvokeAsync<double>("getVisibleHeight", cancellationToken, elementRef.Match()));
public async Task ClickElementAsync(ElementRef elementRef, CancellationToken cancellationToken = default) => await InvokeAsync(async module => await module.InvokeVoidAsync("clickElement", cancellationToken, elementRef.Match()));

// Shadow DOM support
public async Task CreateShadowRootAsync(ElementRef elementRef, string mode = "open", CancellationToken cancellationToken = default) => await InvokeAsync(async module => await module.InvokeVoidAsync("createShadowRoot", cancellationToken, elementRef.Match(), mode));
public async Task InjectStylesheetsAsync(ElementRef elementRef, string[] stylesheets, CancellationToken cancellationToken = default) => await InvokeAsync(async module => await module.InvokeVoidAsync("injectStylesheets", cancellationToken, elementRef.Match(), stylesheets));
public async Task SetupElsaShadowRootAsync(ElementRef elementRef, string[]? customStylesheets = null, CancellationToken cancellationToken = default) => await InvokeAsync(async module => await module.InvokeVoidAsync("setupElsaShadowRoot", cancellationToken, elementRef.Match(), customStylesheets));
public async Task CreateElsaCustomElementAsync(string tagName, string componentName, bool enableShadowDOM = false, CancellationToken cancellationToken = default) => await InvokeAsync(async module => await module.InvokeVoidAsync("createElsaCustomElement", cancellationToken, tagName, componentName, enableShadowDOM));
}
Loading