LogoLogo
HomeDiscourseBlogDiscord
  • Introduction
  • Introduction
    • Quick start
    • Aurelia for new developers
    • Hello world
      • Creating your first app
      • Your first component - part 1: the view model
      • Your first component - part 2: the view
      • Running our app
      • Next steps
  • Templates
    • Template Syntax
      • Attribute binding
      • Event binding
      • Text interpolation
      • Template promises
      • Template references
      • Template variables
      • Globals
    • Custom attributes
    • Value converters (pipes)
    • Binding behaviors
    • Form Inputs
    • CSS classes and styling
    • Conditional Rendering
    • List Rendering
    • Lambda Expressions
    • Local templates (inline templates)
    • SVG
  • Components
    • Component basics
    • Component lifecycles
    • Bindable properties
    • Styling components
    • Slotted content
    • Scope and context
    • CustomElement API
    • Template compilation
      • processContent
      • Extending templating syntax
      • Modifying template parsing with AttributePattern
      • Extending binding language
      • Using the template compiler
      • Attribute mapping
  • Getting to know Aurelia
    • Routing
      • @aurelia/router
        • Getting Started
        • Creating Routes
        • Routing Lifecycle
        • Viewports
        • Navigating
        • Route hooks
        • Router animation
        • Route Events
        • Router Tutorial
        • Router Recipes
      • @aurelia/router-lite
        • Getting started
        • Router configuration
        • Configuring routes
        • Viewports
        • Navigating
        • Lifecycle hooks
        • Router hooks
        • Router events
        • Navigation model
        • Transition plan
    • App configuration and startup
    • Enhance
    • Template controllers
    • Understanding synchronous binding
    • Dynamic composition
    • Portalling elements
    • Observation
      • Observing property changes with @observable
      • Effect observation
      • HTML observation
      • Using observerLocator
    • Watching data
    • Dependency injection (DI)
    • App Tasks
    • Task Queue
    • Event Aggregator
  • Developer Guides
    • Animation
    • Testing
      • Overview
      • Testing attributes
      • Testing components
      • Testing value converters
      • Working with the fluent API
      • Stubs, mocks & spies
    • Logging
    • Building plugins
    • Web Components
    • UI virtualization
    • Errors
      • 0001 to 0023
      • 0088 to 0723
      • 0901 to 0908
    • Bundlers
    • Recipes
      • Apollo GraphQL integration
      • Auth0 integration
      • Containerizing Aurelia apps with Docker
      • Cordova/Phonegap integration
      • CSS-in-JS with Emotion
      • DOM style injection
      • Firebase integration
      • Markdown integration
      • Multi root
      • Progress Web Apps (PWA's)
      • Securing an app
      • SignalR integration
      • Strongly-typed templates
      • TailwindCSS integration
      • WebSockets Integration
      • Web Workers Integration
    • Playground
      • Binding & Templating
      • Custom Attributes
        • Binding to Element Size
      • Integration
        • Microsoft FAST
        • Ionic
    • Migrating to Aurelia 2
      • For plugin authors
      • Side-by-side comparison
    • Cheat Sheet
  • Aurelia Packages
    • Validation
      • Validation Tutorial
      • Plugin Configuration
      • Defining & Customizing Rules
      • Architecture
      • Tagging Rules
      • Model Based Validation
      • Validation Controller
      • Validate Binding Behavior
      • Displaying Errors
      • I18n Internationalization
      • Migration Guide & Breaking Changes
    • i18n Internationalization
    • Fetch Client
      • Overview
      • Setup and Configuration
      • Response types
      • Working with forms
      • Intercepting responses & requests
      • Advanced
    • Event Aggregator
    • State
    • Store
      • Configuration and Setup
      • Middleware
    • Dialog
  • Tutorials
    • Building a ChatGPT inspired app
    • Building a realtime cryptocurrency price tracker
    • Building a todo application
    • Building a weather application
    • Building a widget-based dashboard
    • React inside Aurelia
    • Svelte inside Aurelia
    • Synthetic view
    • Vue inside Aurelia
  • Community Contribution
    • Joining the community
    • Code of conduct
    • Contributor guide
    • Building and testing aurelia
    • Writing documentation
    • Translating documentation
Powered by GitBook
On this page
  • When to Use Synthetic Views
  • Basic Concepts
  • Prerequisites
  • Getting Started
  • Working with Scopes and Bindings
  • Basic Scope Handling
  • Working with Parent Scopes
  • Handling Dynamic Updates
  • Best Practices for Scope Management
  • Real-World Examples
  • 1. Server-Rendered Content with Aurelia Bindings
  • 2. Dynamic Form Builder with Validation
  • Advanced Topics and Patterns
  • 1. Dynamic Component Loading with Lazy Loading
  • 2. Interactive Dashboard Builder

Was this helpful?

Export as PDF
  1. Tutorials

Synthetic view

Learn how you can dynamically synthesize views from templates generated on runtime.

Synthetic views in Aurelia 2 are a powerful feature that allows developers to create and manage views programmatically at runtime. Unlike traditional views in HTML templates, synthetic views offer complete control over view creation, binding, and lifecycle management.

When to Use Synthetic Views

Synthetic views are particularly useful when:

  • Rendering server-generated HTML with Aurelia bindings

  • Dynamically creating templates based on runtime data

  • Integrating with CMS or third-party content that needs Aurelia binding capabilities

  • Creating complex, programmatically-generated UIs

Basic Concepts

A synthetic view in Aurelia 2 consists of several key components:

  1. Template Creation: A programmatically created template element containing your dynamic HTML

  2. Render Location: A marker in the DOM where Aurelia will render your view

  3. View Factory: Creates view instances from your template

  4. Scope: Provides the binding context for your view

  5. View Instance: The actual view that gets rendered and managed

Prerequisites

Before working with synthetic views, you should be familiar with:

  • Creating a new Aurelia app. This tutorial won't cover this; you can look at the other tutorials.

Getting Started

Here's a basic example of creating a synthetic view:

import { 
    convertToRenderLocation, 
    CustomElementDefinition, 
    ICustomElementController, 
    ISyntheticView 
} from '@aurelia/runtime-html';
import { Scope } from '@aurelia/runtime';
import { 
    customElement, 
    IContainer, 
    ViewFactory,
    IPlatform, 
    resolve 
} from "aurelia";

@customElement({
    name: 'synthetic-example',
    template: '<div ref="container"></div>'
})
export class SyntheticExample {
    private container: HTMLElement;
    private view: ISyntheticView;
    private readonly platform = resolve(IPlatform);
    private readonly diContainer = resolve(IContainer);

    $controller!: ICustomElementController<this>;

    async attached() {
        // 1. Create template
        const template = this.platform.document.createElement('template');
        template.innerHTML = `
            <div>
                <h1>\${title}</h1>
                <button click.trigger="handleClick()">Click Me</button>
            </div>
        `;

        // 2. Add to DOM and create render location
        this.container.appendChild(template);
        const renderLocation = convertToRenderLocation(template);

        // 3. Create view factory
        const factory = new ViewFactory(
            this.diContainer,
            CustomElementDefinition.create({
                name: 'dynamic-view',
                template
            })
        );

        // 4. Create view instance
        this.view = factory.create(this.$controller)
            .setLocation(renderLocation);

        // 5. Create scope and activate view
        const viewModel = {
            title: 'Dynamic View',
            handleClick: () => console.log('Clicked!')
        };

        await this.view.activate(
            this.view,
            this.$controller,
            Scope.create(viewModel)
        );
    }

    async detaching() {
        if (this.view) {
            await this.view.deactivate(this.view, this.$controller);
        }
    }
}

This code demonstrates the basic pattern for creating and managing synthetic views in Aurelia 2. Each step is essential:

  1. Template Creation: Create a template element with your dynamic content

  2. DOM Integration: Add the template to the DOM and create a render location

  3. Factory Creation: Create a view factory from your template

  4. View Creation: Create a view instance and set its location

  5. Activation: Activate the view with a scope containing your view model

  6. Cleanup: Properly deactivate the view when done

Working with Scopes and Bindings

Understanding how to handle scopes and bindings properly is crucial when creating synthetic views. The scope provides the context for all bindings within your dynamic template.

Basic Scope Handling

Here's how to create and use scopes effectively:

@customElement({
    name: 'scope-example',
    template: '<div ref="container"></div>'
})
export class ScopeExample {
    private container: HTMLElement;
    private view: ISyntheticView;
    private readonly platform = resolve(IPlatform);
    private readonly diContainer = resolve(IContainer);

    $controller!: ICustomElementController<this>;

    async attached() {
        const template = this.platform.document.createElement('template');
        template.innerHTML = `
            <div>
                <!-- Basic property binding -->
                <h1>\${title}</h1>
                
                <!-- Event binding -->
                <button click.trigger="handleClick()">Click Me</button>
                
                <!-- Two-way binding -->
                <input value.bind="inputValue">
                <p>You typed: \${inputValue}</p>
                
                <!-- Repeater binding -->
                <ul>
                    <li repeat.for="item of items">\${item.name}</li>
                </ul>
            </div>
        `;

        this.container.appendChild(template);
        const renderLocation = convertToRenderLocation(template);

        const factory = new ViewFactory(
            this.diContainer,
            CustomElementDefinition.create({
                name: 'scope-example-view',
                template
            })
        );

        this.view = factory.create(this.$controller)
            .setLocation(renderLocation);

        // Create view model with reactive properties
        const viewModel = {
            title: 'Dynamic View with Bindings',
            inputValue: '',
            items: [
                { name: 'Item 1' },
                { name: 'Item 2' }
            ],
            handleClick: () => {
                console.log('Current input:', viewModel.inputValue);
            }
        };

        await this.view.activate(
            this.view,
            this.$controller,
            Scope.create(viewModel)
        );
    }
}

Working with Parent Scopes

Sometimes, you need to access the parent component's scope in your synthetic view. Here's how to do that:

@customElement({
    name: 'parent-scope-example',
    template: '<div ref="container"></div>'
})
export class ParentScopeExample {
    // Parent component property
    message = 'Hello from parent';

    private container: HTMLElement;
    private view: ISyntheticView;
    private readonly platform = resolve(IPlatform);
    private readonly diContainer = resolve(IContainer);

    $controller!: ICustomElementController<this>;

    async attached() {
        const template = this.platform.document.createElement('template');
        template.innerHTML = `
            <div>
                <!-- Access parent scope -->
                <h2>\${parentMessage}</h2>
                
                <!-- Access local scope -->
                <p>\${localMessage}</p>
                
                <button click.trigger="handleClick()">
                    Update Messages
                </button>
            </div>
        `;

        this.container.appendChild(template);
        const renderLocation = convertToRenderLocation(template);

        const factory = new ViewFactory(
            this.diContainer,
            CustomElementDefinition.create({
                name: 'parent-scope-view',
                template
            })
        );

        this.view = factory.create(this.$controller)
            .setLocation(renderLocation);

        // Create child scope with parent scope access
        const childScope = Scope.create(
            {
                localMessage: 'Hello from child',
                handleClick: () => {
                    // Can access both scopes
                    childScope.localMessage = 'Updated child message';
                    this.message = 'Updated parent message';
                }
            },
            this.$controller.scope // Parent scope
        );

        await this.view.activate(
            this.view,
            this.$controller,
            childScope
        );
    }
}

Handling Dynamic Updates

When your view model data changes, the bindings will automatically update. However, if you need to rebuild the view completely, you'll need to handle that manually:

@customElement({
    name: 'dynamic-update-example',
    template: '<div ref="container"></div>'
})
export class DynamicUpdateExample {
    private container: HTMLElement;
    private view: ISyntheticView;
    private readonly platform = resolve(IPlatform);
    private readonly diContainer = resolve(IContainer);

    $controller!: ICustomElementController<this>;

    async updateView(newData: any) {
        // Deactivate existing view if it exists
        if (this.view) {
            await this.view.deactivate(this.view, this.$controller);
            this.container.innerHTML = ''; // Clear container
        }

        // Create new view with updated data
        const template = this.platform.document.createElement('template');
        template.innerHTML = `
            <div>
                <h2>\${title}</h2>
                <div repeat.for="item of items">
                    \${item.name}
                </div>
            </div>
        `;

        this.container.appendChild(template);
        const renderLocation = convertToRenderLocation(template);

        const factory = new ViewFactory(
            this.diContainer,
            CustomElementDefinition.create({
                name: 'dynamic-update-view',
                template
            })
        );

        this.view = factory.create(this.$controller)
            .setLocation(renderLocation);

        await this.view.activate(
            this.view,
            this.$controller,
            Scope.create(newData)
        );
    }
}

Best Practices for Scope Management

  1. Cleanup: Always deactivate views when they're no longer needed

  2. Scope Isolation: Create isolated scopes when you don't need parent scope access

  3. Parent Scope Access: Use parent scopes judiciously to avoid tight coupling

  4. Memory Management: Clear references to views and scopes when disposing

  5. Error Handling: Wrap view creation and activation in try-catch blocks

async createView(data: any) {
    try {
        // ... view creation code ...
        
        await this.view.activate(
            this.view,
            this.$controller,
            Scope.create(data)
        );
    } catch (error) {
        console.error('Failed to create view:', error);
        // Handle error appropriately
    }
}

async detaching() {
    try {
        if (this.view) {
            await this.view.deactivate(this.view, this.$controller);
            this.view = null;
        }
    } catch (error) {
        console.error('Failed to cleanup view:', error);
    }
}

Real-World Examples

1. Server-Rendered Content with Aurelia Bindings

This example shows how to take HTML content from a server (like a CMS) and make it interactive with Aurelia bindings:

@customElement({
    name: 'cms-content',
    template: '<div ref="container"></div>'
})
export class CmsContent {
    private container: HTMLElement;
    private view: ISyntheticView;
    private readonly platform = resolve(IPlatform);
    private readonly diContainer = resolve(IContainer);

    $controller!: ICustomElementController<this>;

    async attached() {
        try {
            // Fetch content from CMS/server
            const response = await fetch('/api/content/page-123');
            const html = await response.text();
            
            await this.renderContent(html);
        } catch (error) {
            console.error('Failed to load content:', error);
        }
    }

    private async renderContent(html: string) {
        const template = this.platform.document.createElement('template');
        
        // Server returns HTML with Aurelia binding expressions
        // Example: <button click.trigger="handleAction('${id}')">...</button>
        template.innerHTML = html;

        this.container.appendChild(template);
        const renderLocation = convertToRenderLocation(template);

        const factory = new ViewFactory(
            this.diContainer,
            CustomElementDefinition.create({
                name: 'cms-content-view',
                template
            })
        );

        this.view = factory.create(this.$controller)
            .setLocation(renderLocation);

        // Create view model with methods that the server-rendered content can bind to
        const viewModel = {
            handleAction: (id: string) => {
                console.log('Action triggered:', id);
            },
            submitForm: async (formData: any) => {
                const response = await fetch('/api/submit', {
                    method: 'POST',
                    body: JSON.stringify(formData)
                });
                return response.json();
            }
        };

        await this.view.activate(
            this.view,
            this.$controller,
            Scope.create(viewModel)
        );
    }

    async detaching() {
        if (this.view) {
            await this.view.deactivate(this.view, this.$controller);
        }
    }
}

2. Dynamic Form Builder with Validation

This example creates forms dynamically based on a schema, with validation:

interface FormField {
    type: 'text' | 'number' | 'select' | 'date';
    name: string;
    label: string;
    required?: boolean;
    options?: { value: string; label: string; }[];
    validation?: {
        pattern?: string;
        min?: number;
        max?: number;
        message?: string;
    };
}

@customElement({
    name: 'dynamic-form',
    template: '<div ref="container"></div>'
})
export class DynamicForm {
    private container: HTMLElement;
    private view: ISyntheticView;
    private readonly platform = resolve(IPlatform);
    private readonly diContainer = resolve(IContainer);

    @bindable() schema: FormField[] = [];
    @bindable() onSubmit: (data: any) => Promise<void>;

    $controller!: ICustomElementController<this>;

    async schemaChanged() {
        await this.renderForm();
    }

    private generateFieldHtml(field: FormField): string {
        const validationAttrs = field.validation ? `
            pattern="${field.validation.pattern || ''}"
            min="${field.validation.min || ''}"
            max="${field.validation.max || ''}"
        ` : '';

        switch (field.type) {
            case 'select':
                return `
                    <div class="form-group">
                        <label for="${field.name}">${field.label}</label>
                        <select 
                            class="form-control \${errors.${field.name} ? 'is-invalid' : ''}"
                            id="${field.name}"
                            value.bind="formData.${field.name}"
                            required.bind="${field.required}"
                        >
                            <option value="">Select...</option>
                            ${field.options?.map(opt => 
                                `<option value="${opt.value}">${opt.label}</option>`
                            ).join('')}
                        </select>
                        <div class="invalid-feedback">
                            \${errors.${field.name}}
                        </div>
                    </div>
                `;
            
            default:
                return `
                    <div class="form-group">
                        <label for="${field.name}">${field.label}</label>
                        <input 
                            type="${field.type}"
                            class="form-control \${errors.${field.name} ? 'is-invalid' : ''}"
                            id="${field.name}"
                            value.bind="formData.${field.name}"
                            required.bind="${field.required}"
                            ${validationAttrs}
                        >
                        <div class="invalid-feedback">
                            \${errors.${field.name}}
                        </div>
                    </div>
                `;
        }
    }

    private async renderForm() {
        if (this.view) {
            await this.view.deactivate(this.view, this.$controller);
            this.container.innerHTML = '';
        }

        const template = this.platform.document.createElement('template');
        template.innerHTML = `
            <form submit.trigger="submitForm($event)">
                ${this.schema.map(field => 
                    this.generateFieldHtml(field)
                ).join('')}
                <button type="submit" class="btn btn-primary">
                    Submit
                </button>
            </form>
        `;

        this.container.appendChild(template);
        const renderLocation = convertToRenderLocation(template);

        const factory = new ViewFactory(
            this.diContainer,
            CustomElementDefinition.create({
                name: 'dynamic-form-view',
                template
            })
        );

        this.view = factory.create(this.$controller)
            .setLocation(renderLocation);

        const viewModel = {
            formData: {},
            errors: {},
            validateField: (name: string, value: any) => {
                const field = this.schema.find(f => f.name === name);
                if (!field) return;

                if (field.required && !value) {
                    viewModel.errors[name] = 'This field is required';
                    return false;
                }

                if (field.validation) {
                    // Add validation logic here
                    // Return false if validation fails
                }

                delete viewModel.errors[name];
                return true;
            },
            submitForm: async (event: Event) => {
                event.preventDefault();
                
                // Validate all fields
                let isValid = true;
                for (const field of this.schema) {
                    const valid = viewModel.validateField(
                        field.name, 
                        viewModel.formData[field.name]
                    );
                    if (!valid) isValid = false;
                }

                if (!isValid) return;

                if (this.onSubmit) {
                    await this.onSubmit(viewModel.formData);
                }
            }
        };

        await this.view.activate(
            this.view,
            this.$controller,
            Scope.create(viewModel)
        );
    }
}

Usage:

@customElement({
    template: `
        <dynamic-form 
            schema.bind="formSchema"
            on-submit.call="handleSubmit($event)">
        </dynamic-form>
    `
})
export class FormPage {
    formSchema: FormField[] = [
        {
            type: 'text',
            name: 'username',
            label: 'Username',
            required: true,
            validation: {
                pattern: '^[a-zA-Z0-9]{3,}$',
                message: 'Username must be at least 3 characters'
            }
        },
        {
            type: 'select',
            name: 'role',
            label: 'Role',
            required: true,
            options: [
                { value: 'user', label: 'User' },
                { value: 'admin', label: 'Admin' }
            ]
        }
    ];

    async handleSubmit(formData: any) {
        try {
            await fetch('/api/users', {
                method: 'POST',
                body: JSON.stringify(formData)
            });
        } catch (error) {
            console.error('Submit failed:', error);
        }
    }
}

Advanced Topics and Patterns

1. Dynamic Component Loading with Lazy Loading

This example shows how to load and render components based on server configuration dynamically:

interface ComponentConfig {
    type: string;
    props: Record<string, any>;
    template: string;
    scriptUrl?: string;
}

@customElement({
    name: 'component-loader',
    template: '<div ref="container"></div>'
})
export class ComponentLoader {
    private container: HTMLElement;
    private view: ISyntheticView;
    private readonly platform = resolve(IPlatform);
    private readonly diContainer = resolve(IContainer);

    $controller!: ICustomElementController<this>;

    async loadComponent(config: ComponentConfig) {
        try {
            // Load external script if needed
            if (config.scriptUrl) {
                await this.loadScript(config.scriptUrl);
            }

            await this.renderComponent(config);
        } catch (error) {
            console.error('Failed to load component:', error);
        }
    }

    private async loadScript(url: string): Promise<void> {
        return new Promise((resolve, reject) => {
            const script = document.createElement('script');
            script.src = url;
            script.onload = () => resolve();
            script.onerror = () => reject(new Error(`Failed to load script: ${url}`));
            document.head.appendChild(script);
        });
    }

    private async renderComponent(config: ComponentConfig) {
        if (this.view) {
            await this.view.deactivate(this.view, this.$controller);
            this.container.innerHTML = '';
        }

        const template = this.platform.document.createElement('template');
        template.innerHTML = config.template;

        this.container.appendChild(template);
        const renderLocation = convertToRenderLocation(template);

        const factory = new ViewFactory(
            this.diContainer,
            CustomElementDefinition.create({
                name: `dynamic-component-${Date.now()}`,
                template
            })
        );

        this.view = factory.create(this.$controller)
            .setLocation(renderLocation);

        await this.view.activate(
            this.view,
            this.$controller,
            Scope.create(config.props)
        );
    }
}

2. Interactive Dashboard Builder

This example demonstrates building a dashboard with draggable widgets:

interface DashboardWidget {
    id: string;
    type: 'chart' | 'table' | 'metrics';
    position: { x: number; y: number };
    size: { width: number; height: number };
    config: Record<string, any>;
}

@customElement({
    name: 'dashboard-builder',
    template: '<div ref="container" class="dashboard-container"></div>'
})
export class DashboardBuilder {
    private container: HTMLElement;
    private view: ISyntheticView;
    private readonly platform = resolve(IPlatform);
    private readonly diContainer = resolve(IContainer);

    $controller!: ICustomElementController<this>;
    private widgets: DashboardWidget[] = [];

    private generateWidgetHtml(widget: DashboardWidget): string {
        const style = `
            position: absolute;
            left: ${widget.position.x}px;
            top: ${widget.position.y}px;
            width: ${widget.size.width}px;
            height: ${widget.size.height}px;
        `;

        return `
            <div class="widget" style="${style}"
                 draggable="true"
                 dragstart.trigger="startDrag($event, '${widget.id}')"
                 dragend.trigger="endDrag($event)">
                <div class="widget-header">
                    ${widget.type.toUpperCase()}
                    <button click.trigger="removeWidget('${widget.id}')">×</button>
                </div>
                <div class="widget-content">
                    ${this.generateWidgetContent(widget)}
                </div>
            </div>
        `;
    }

    private generateWidgetContent(widget: DashboardWidget): string {
        switch (widget.type) {
            case 'chart':
                return `
                    <canvas ref="chart-${widget.id}"
                           afterAttach.call="initChart($event, '${widget.id}')">
                    </canvas>
                `;
            case 'table':
                return `
                    <table class="table">
                        <thead>
                            <tr>
                                ${widget.config.columns.map(col => 
                                    `<th>${col.header}</th>`
                                ).join('')}
                            </tr>
                        </thead>
                        <tbody>
                            <tr repeat.for="row of widget.config.data">
                                ${widget.config.columns.map(col => 
                                    `<td>\${row[col.field]}</td>`
                                ).join('')}
                            </tr>
                        </tbody>
                    </table>
                `;
            case 'metrics':
                return `
                    <div class="metrics-grid">
                        ${widget.config.metrics.map(metric => `
                            <div class="metric-card">
                                <div class="metric-value">\${${metric.value}}</div>
                                <div class="metric-label">${metric.label}</div>
                            </div>
                        `).join('')}
                    </div>
                `;
            default:
                return '';
        }
    }

    async attached() {
        await this.renderDashboard();
    }

    private async renderDashboard() {
        const template = this.platform.document.createElement('template');
        template.innerHTML = `
            <div class="dashboard">
                ${this.widgets.map(widget => 
                    this.generateWidgetHtml(widget)
                ).join('')}
                <div class="dashboard-controls">
                    <button click.trigger="addWidget('chart')">Add Chart</button>
                    <button click.trigger="addWidget('table')">Add Table</button>
                    <button click.trigger="addWidget('metrics')">Add Metrics</button>
                    <button click.trigger="saveDashboard()">Save Layout</button>
                </div>
            </div>
        `;

        this.container.appendChild(template);
        const renderLocation = convertToRenderLocation(template);

        const factory = new ViewFactory(
            this.diContainer,
            CustomElementDefinition.create({
                name: 'dashboard-view',
                template
            })
        );

        this.view = factory.create(this.$controller)
            .setLocation(renderLocation);

        const viewModel = {
            widgets: this.widgets,
            startDrag: (event: DragEvent, widgetId: string) => {
                event.dataTransfer.setData('widgetId', widgetId);
            },
            endDrag: (event: DragEvent) => {
                const widgetId = event.dataTransfer.getData('widgetId');
                const widget = this.widgets.find(w => w.id === widgetId);
                if (widget) {
                    widget.position = {
                        x: event.clientX - this.container.offsetLeft,
                        y: event.clientY - this.container.offsetTop
                    };
                    this.renderDashboard();
                }
            },
            addWidget: async (type: DashboardWidget['type']) => {
                const widget: DashboardWidget = {
                    id: `widget-${Date.now()}`,
                    type,
                    position: { x: 0, y: 0 },
                    size: { width: 300, height: 200 },
                    config: await this.getDefaultConfig(type)
                };
                this.widgets.push(widget);
                await this.renderDashboard();
            },
            removeWidget: async (widgetId: string) => {
                this.widgets = this.widgets.filter(w => w.id !== widgetId);
                await this.renderDashboard();
            },
            initChart: (element: HTMLCanvasElement, widgetId: string) => {
                const widget = this.widgets.find(w => w.id === widgetId);
                if (widget && widget.type === 'chart') {
                    // Initialize chart using your preferred library
                    // Example with Chart.js:
                    new Chart(element, widget.config.chartConfig);
                }
            },
            saveDashboard: async () => {
                try {
                    await fetch('/api/dashboard', {
                        method: 'POST',
                        headers: { 'Content-Type': 'application/json' },
                        body: JSON.stringify(this.widgets)
                    });
                } catch (error) {
                    console.error('Failed to save dashboard:', error);
                }
            }
        };

        await this.view.activate(
            this.view,
            this.$controller,
            Scope.create(viewModel)
        );
    }

    private async getDefaultConfig(type: DashboardWidget['type']) {
        // Return default configuration based on widget type
        switch (type) {
            case 'chart':
                return {
                    chartConfig: {
                        // Default chart configuration
                    }
                };
            case 'table':
                return {
                    columns: [
                        { field: 'id', header: 'ID' },
                        { field: 'name', header: 'Name' }
                    ],
                    data: []
                };
            case 'metrics':
                return {
                    metrics: [
                        { label: 'Total', value: 0 },
                        { label: 'Average', value: 0 }
                    ]
                };
        }
    }
}
PreviousSvelte inside AureliaNextVue inside Aurelia

Last updated 3 months ago

Was this helpful?

You have familiarized yourself with the .

You have familiarized yourself with .

You are familiar with . You don't need to master it; you need to be familiar with its existence and why it matters in Aurelia.

Native Web APIs, such as .

Aurelia template syntax
components in Aurelia
Dependency Injection
createElement