dot CMS

Integrating Universal Visual Editor (UVE) with Any JavaScript Framework: Concepts and Approaches

Integrating Universal Visual Editor (UVE) with Any JavaScript Framework: Concepts and Approaches
Author image

Freddy Montes

Product Manager

Share this article on:

As a developer, one of the most important things I need is freedom to innovate. There are numerous tools available, including AI, frameworks, and various languages, all designed to accelerate my work.

We value having choices in the tools we use, and this is why we developed the Universal Visual Editor (UVE). UVE is an innovative feature in dotCMS that allows non-developers to directly edit pages with a consistent, WYSIWYG-like experience, regardless of the rendering framework. This flexibility aligns with the principles of a Universal CMS, empowering developers with JavaScript tools while offering an intuitive interface for editors and marketing teams. Our goal is to deliver an exceptional real-time, context-based editing experience for content authors — a philosophy that is the cornerstone of UVE.

Embracing Flexibility in Web Development

Imagine the flexibility of having a universal editing tool provide the same experience across your chosen development frameworks. UVE provides this luxury, offering a robust interface that adapts to various technologies without compromising functionality or creativity.

Why Framework Independence Matters

UVE's framework independence is a game changer for developers and organizations leveraging multiple tools. It allows you to continue using your preferred frameworks while accessing powerful editing capabilities, ensuring consistency in development workflows regardless of the tech stack.

Our very own JavaScript SDK

Understanding your web app’s architecture is essential before you start building. dotCMS also provides a JavaScript SDK that will help you and your team deploy the application more quickly.

​​The dotCMS JavaScript Software Development Kit (SDK) is a group of JavaScript libraries created to facilitate the building of headless pages and the integration with popular front-end frameworks. The SDKs simplify the process of data retrieval through dotCMS REST APIs, enabling developers to concentrate on their front-end code for faster development.

However, even if no pre-built library exists for your preferred framework, you can work with the UVE.

Architecture Components: The Foundation of UVE

UVE’s well-structured architecture, outlined below, is designed to work seamlessly with any framework — whether JavaScript or otherwise — regardless of whether you need to render the page in the server, in the client, or statically build it.

Layout Engine

UVE's layout engine follows a standardized 12-column grid system, enabling flexible and responsive design. This system ensures that content is organized in a predictable manner, supporting a wide range of layouts, from simple to complex.

Grid System Overview

The grid system supports native CSS Grid or any CSS library grid system — including the big favorite, Tailwind.

Column Management

Page layouts are structured as a series of rows, which specify vertical placement, that contain columns, which specify horizontal placement in terms of the 12-column grid.

Columns are defined with properties like leftOffset (starting position) and width (span). This allows for precise control over content placement, ensuring a functional layout. 

For example:

{

  "layout": {

    "body": {

      "rows": [

        {

          "columns": [

            {

              "leftOffset": 1,

              "width": 3,

              "containers": [

                { "identifier": "container-id", "uuid": "container-uuid" }

              ]

            }

          ]

        }

      ]

    }

  }

}

With those parameters, the column will comprise a box spanning the second through fourth positions in the grid, occupying the left side of the layout.

Container Management: Where Content Meets Structure

Containers act as the bridge between your application's layout structure and its actual content. Each container holds specific content through unique identifiers, enabling dynamic management of displayed items.

Example of Container Structure:

{

  "containers": {

    "5f4d-cc3b-5aa7-65d6-d832": {

      "contentlets": [

        "uuid-2": [

          { "contentType": "Banner", "title": "Header" },

          { "contentType": "Card", "text": "Key Features" }

        ]

      ]

    }

  }

}
  • 5f4d-cc3b-5aa7-65d6-d832: this key represents a container identifier

  • uuid-2: is the representation of the container in a specific position of the layout.

Editor Communication Layer: The Heart of Integration

The editor communicates with your application through the standard postMessage API, providing real-time updates without requiring too much effort and custom modification in your web app.

Grid System Implementation

CSS Grid Syntax Example:

.row {

    display: grid;

    grid-template-columns: repeat(12, 1fr);

    gap: 1rem;

}

/* Where "n" represents the position */

.col-start-n {

    grid-column-start: n;

}

.col-end-n {

    grid-column-end: n;

}

Container Management in Detail

Each container maintains its own set of content, allowing for independent management. This modular approach enhances flexibility and scalability, as containers can be reused across different parts of the application.

Contentlet Management

Contentlets (individual units of content) are managed within containers, providing a clear separation between structure and content. This allows for efficient updates and rendering, ensuring optimal performance.

Component Registry: Mapping Content to UI

A registry system maps specific content types (e.g., Blog Post) to their corresponding UI components:

import {BlogPost, Product} from '@components'

const componentRegistry = {

  'BlogPost': BlogPost,

  'Product': Product,

  // Additional mappings as needed

};

This system ensures that each content type is rendered correctly, providing a consistent user experience across different frameworks.

Editor Communication in Practice

The editor communicates with the application using postMessage, which is efficient and compatible with any framework:

// Detecting editor mode

if (window.parent !== window) {

  window.addEventListener('message', (event) => {

    if (event.data.type === 'CONTENT_UPDATE') {

      // Handle content updates

    }

    // Similar handling for other events

  });

}

Real-World Benefits: Enhancing Productivity

Integrating UVE with your JavaScript framework offers numerous benefits, including:

  1. Framework Independence: You'll maintain existing workflows and development tools without requiring significant changes.

  2. Performance Optimization: Efficient content updates minimize page reloads, improving user experience.

  3. Developer Flexibility: You can implement custom components tailored to specific needs while leveraging UVE's powerful features.

Getting Started: Implementing UVE in Your Application

Let's walk through the key steps to integrate the Universal Visual Editor with any JavaScript framework.

1. Setting Up the Router

First, implement a catch-all route to handle all dotCMS pages:

// React with React Router

function App() {

  return (

    <Router>

      <Routes>

        <Route path="*" element={<DotCMSPage />} />

      </Routes>

    </Router>

  );

}
// Angular (app-routing.module.ts)

const routes: Routes = [

  { path: '**', component: DotCMSPageComponent }

];

For file-based routing like Next.js, for example, you will create a page to catch all routes: /app/[[...slug]]/page.js. 

2. Creating Your Component Registry

Map your content types to their corresponding UI components:

// React

import { BlogComponent } from './components'

const componentRegistry = {

  'Blog': BlogComponent,

  'Product': ProductComponent,

  'default': FallbackComponent

};
// Angular

@Injectable()

export class ComponentRegistry {

  private registry = {

    'Blog': BlogComponent,

    'Product': ProductComponent,

    'default': FallbackComponent

  };

  resolve(type: string): Type<any> {

    return this.registry[type] || this.registry['default'];

  }

}

3. Implementing the Layout Engine

The layout engine translates dotCMS's hierarchical structure (rows > columns > containers) into framework components:

// React Implementation

function Row({ row }) {

  return (

    <div style={{

      display: 'grid',

      gridTemplateColumns: 'repeat(12, 1fr)',

      gap: '1rem'

    }}>

      {row.columns.map((column, index) => (

        <Column key={index} {...column} />

      ))}

    </div>

  );

}



function Column({ leftOffset, width, containers }) {

  return (

    <div style={{

      gridColumnStart: leftOffset,

      gridColumnEnd: leftOffset + width

    }}>

      {containers.map((container, index) => (

        <Container 

          key={index} 

          identifier={container.identifier}

          uuid={container.uuid} 

        />

      ))}

    </div>

  );

}

function Container({ identifier, uuid }) {

  return (

    <div 

      data-dot-object="container"

      data-dot-identifier={identifier}

      data-dot-uuid={uuid}

    >

      {/* Contentlets rendering based on container data */}

    </div>

  );

}
// Angular Implementation

@Component({

  selector: 'app-row',

  template: `

    <div [ngStyle]="{

      display: 'grid',

      gridTemplateColumns: 'repeat(12, 1fr)',

      gap: '1rem'

    }">

        @for (let col of row.columns; track col.id) {

            <app-column [column]="col"></app-column>

        }

    </div>

  `

})

export class RowComponent {

  @Input() row: any;

}




@Component({

  selector: 'app-column',

  template: `

    <div

      [style.grid-column-start]="column.leftOffset"

      [style.grid-column-end]="column.leftOffset + column.width"

    >

      @for (let container of column.containers; track container.id) {

        <app-container 

          [identifier]="container.identifier" 

          [uuid]="container.uuid">

        </app-container>

      }

    </div>

  `

})

export class ColumnComponent {

  @Input() column: any;

}

4. Container Component

Now inside the column container you will create a container to manage contentlets:

// React Container

function Container({ identifier, uuid }) {

  return (

    <div

      data-dot-object="container"

      data-dot-identifier={identifier}

      data-dot-uuid={uuid}>

      {contentlets.map(contentlet => {

        const Component = componentRegistry[contentlet.type];

        return <Component key={contentlet.id} {...contentlet} />;

      })}

    </div>

  );

}
// Angular Container

@Component({

  selector: 'dot-container',

  template: `

    <div

      [attr.data-dot-object]="'container'"

      [attr.data-dot-identifier]="identifier"

      [attr.data-dot-uuid]="uuid">

      <ng-container *ngFor="let content of contentlets">

        <ng-container *ngComponentOutlet="

          registry.resolve(content.type);

          inputs: { data: content }

        ">

        </ng-container>

      </ng-container>

    </div>

  `

})

5. Editor Communication

Set up the communication between your page and the editor:

// React Hook

function useEditor() {

  useEffect(() => {

    if (window.parent === window) return;    

    const handleMessage = (event) => {

      switch (event.data.type) {

        case 'CONTENT_UPDATE':

          // Handle update

          break;

      }

    };


    window.addEventListener('message', handleMessage);

    return () => window.removeEventListener('message', handleMessage);

  }, []);

}
// Angular Service

@Injectable()

export class EditorService {

  constructor(

    @Inject(PLATFORM_ID)

    private platformId: any

  )

    if (isPlatformBrowser(this.platformId) && window.parent !== window) {

      window.addEventListener('message', this.handleMessage);

    }

  }


  private handleMessage(event: MessageEvent) {

    if (event.data.type === 'CONTENT_UPDATE') {

      // Handle update

    }

  }

}

These examples demonstrate the core concepts while remaining framework specific. The key is understanding that regardless of your chosen framework, the underlying principles remain the same:

  • Single entry point routing

  • Component-based content rendering

  • Grid-based layout system

  • Event-driven editor communication

Best Practices for Success

1. Keep Components Independent

Ensure each component is self-contained and focused on its specific task.

2. Implement Fallbacks

Provide fallback components to maintain functionality when content types are undefined.

3. Optimize Performance

Leverage lazy loading and efficient updates to enhance user experience.

4. Maintain Clean Architecture

Separate application logic from UVE integration for easier maintenance.

Conclusion: The Future of UVE

We’re very excited to have created an SDK that can greatly accelerate your implementation and time to market. Still, it’s important to understand the concepts outlined above if you want to employ a technology not included in our libraries — or, more accurately, not included yet.

The UVE continues to evolve, promising even greater flexibility and enhanced features. By maintaining a framework-agnostic approach, we ensure that developers can integrate this powerful tool into their projects seamlessly, unlocking new possibilities for creating intuitive and efficient content management systems.

Next Steps

Ready to explore the integration? Here are some resources to get you started:

  1. JavaScript SDK Documentation – Comprehensive guide to using our JavaScript SDK for seamless integration.

  2. Our Client Library – Simplify your interactions with dotCMS APIs.

  3. Framework-specific libraries:

  1. Implementation examples

Our team is committed to supporting every step of your journey with dotCMS. Start experimenting today and watch how UVE transforms your web development experience, blending power with flexibility.