skip to Main Content

I maintain a JS UI component library and a frontend web application following the general MVC architecture. There are basic views, complex views, and then controllers that bind the view logic to the model.

Basic components (views)

// The UI library is made of atomic components like the following one.
// 
// * This could be as well a React component, a native web component, or any other framework.
//   It accepts properties, renders a DOM element, and has a means of informing the world 
//   about user actions and that's all that matters. It's a framework-agnostic example.
// * Note: this is a pseudo-code to keep it simple. 
class ButtonView extends BaseView {
    constructor( type, cssClass, label ) {
        this.type = type;
        this.cssClass = cssClass;
        this.label = label;
        
        // ...
    }
    
    render() {      
        this.element = document.createElement( 'button' );
        
        // ...
        
        // Set DOM element attributes and children so it looks familiar
        // <button type="{ type }" class="{ cssClass }">{ label }</button>
        
        // ...
        
        this.element.addEventListener( 'click' ), () => {
            this.fire( 'click' );
        } );
    }   
}

Complex web application views

// The web application composes UI library views (e.g. simple buttons ☝️) 
// into complex user interfaces like forms, toolbars, etc.
//
// Note: this is a pseudo-code to keep it simple. 
class SomeFormView extends BaseView {
    // ...
    
    render() {      
        // Use the basic component to as a building block for the complex view.
        const button = new ButtonView( 'submit', 'foo', 'Send form' );
        
        button.on( 'click', () => {
            this.submitForm();      
        } );
        
        button.render();
    
        // ...inject the button into the form along with other components.
    }       
    
    submitForm() {
        this.fire( 'submit', { 
            /* Passing the form data along with the event */ 
        } );
    }
    
    // ...
}

Web application controllers

// The controller glues the topmost view layer and the model together.
// 
// Note: this is a pseudo-code to keep it simple. 
class SomeController {
    constructor() {
        this.someFormView = new SomeFormView();
        this.someFormView.render(); 
        this.someFormView.on( 'submit', ( data ) => {
            this.onSubmit( data );
        } );
    }
    
    onSubmit( data ) {
        // E.g. save data to a database. It doesn't really matter.
    }
}

Problem

My business requirement is to make the above MVC architecture as open-ended and extendable as possible by integrators. Here are some details:

  • The web application runs in a web browser.
  • The integrators need an API to:
    • Change the template (add/remove attributes, children, behaviors) for basic components like mentioned ButtonView.
    • Change the template (add/remove attributes, children, behaviors) for complex views like mentioned SomeFormView.
    • Remove, replace, or inject an extra logic to the SomeController. This is especially useful when they have already customized the views the controller would glue to the model.
  • The customization must be possible in the runtime (e.g. right before the application starts on the client). Build-time module replacement and similar solutions are, sadly, off the table.
    • As a side note, there’s an underlying plugin system the integrators can use to extend the application. So there’s a central extension point.

I’m looking for good examples: existing frameworks, books, academic papers or just any sort of inspiration. It does not have to be web-based. I assume that native application developers who allow for 3rd-party plugins (extensions) are facing the same set of problems that I am.

Nothing pops into my head aside from:

  • splitting views and controllers into atomic chunks (e.g. methods)
  • providing some sort of view/controller registry the integrator may hook into before the UI is rendered (and displayed) to swap classes.

Are ideas?

2

Answers


  1. I can help you approach this from an architects way of thinking which might point you in the right direction, but I haven’t been a hands-on JavaScript developer for years, so I can’t offer concrete code you can just implement.

    I have two ideas for you: Principles & Patterns, and Lifecycle & Documentation.

    Principles & Patterns

    The SOLID principles are probably your best bet, particularly Open–closed, Liskov substitution and Dependency inversion.

    Using those you should be able to develop some integration & extension patterns you can offer, e.g.:

    1. Expose some kind of friendly API that exposes parts of your library for extension.
    2. Offer substitution at the method/property level e.g. a base class or some basic implementation that they can extend by overriding.
    3. Offer a code-level interface/contract that can be implemented by the integrators by them providing a totally new implementation.

    Change the template (add/remove attributes, children, behaviors) for basic components like mentioned ButtonView.

    Adding / removing children could be done by exposing a collection of child controls; I’m less clear on how you’d do that with behaviors & attributes, partially because I’m not sure how you’d offer that in a controlled way (I assume you have ‘internal’ behaviors & attributes you’d want to protect) – maybe some kind of facade?

    Change the template (add/remove attributes, children, behaviors) for complex views like mentioned SomeFormView.

    Sounds like #2 or #3 above. You provide a vanilla implementation, they are able to replace it with their own. Maybe part of the definition for that is methods and/or base classes that ensure your ‘under the hood’ plumbing still works.

    Alternatively, you could break-up the template into parts that can be individually changed or extended – but not the overall template.

    Remove, replace, or inject an extra logic to the SomeController. This is especially useful when they have already customized the views the controller would glue to the model.

    I’m not sure how you could offer the ability to modify logic in a controlled way. I guess I’d need to know more like specific examples of what you were trying to do so I could better understand. If you think about cohesion: making changes to a sub-system’s inner-cohesion is usually a serious design task – opening that up to 3rd party modification would be hard as you risk violating the Open-closed Principle.

    In data processing there’s an architectural style called Pipes and Filters; the idea is that you have sequential linear flow of "filters", and naturally these can be added to, etc. If your logic was sufficiently flow-orientated or modular you might be able to offer a way for changing those modules – this implies that the "wiring" of the logic was relatively simple, i.e. simple enough that you could expose it for limited re-organisation.

    I guess one idea is to provide the logic as a set of base classes that are pre-wired, where each class has a specific job (they work cohesively). You could allow extension or replacement of specific classes as long as the signatures between them was preserved.

    Lifecycle & Documentation

    This is really about taking ideas from other framework implementations out there, e.g. the ASP.NET Page Lifecycle.

    No, I’m not suggesting you re-implement that framework, but as someone who used to develop on that stack, I came to appreciate that to make the best use of it you really needed to understand the underlying page lifecycle – which also affected how you designed and implemented controls that ran on those pages.

    There will be other similar lifecycles and technologies out there, so if you can’t bring yourself to study this one, find another one you prefer.

    There’s two idea from this you might find useful:

    • Rather than offering an API here and a base-class there, is there a more cohesive and holistic lifecycle you can provide? It might be that the lifecycle isn’t new – it might simply be you showing how your library works in with the existing lifecycle of the page/DOM model.
    • This is the second idea: documentation – not ‘deep dive’ documentation as in how to do a specific task, but ‘bigger picture’ documentation that helps integrators understand the big picture e.g. how your library works in with the existing lifecycle of the page/DOM model. The kind of thing you are trying to achieve is complex, so augmenting your code-level solution with big picture documentation might help integrators understand how they need to work to get the best out of your library.

    Lastly, I’m sure you will have thought about it, but make sure you include things that help integrators debug. The could be some combination of documentation, logging, conventions, etc.

    Login or Signup to reply.
  2. I am sure you have already thought through SOLID principles. The way you have described your code seems very similar to VS Code extensions framework.

    splitting views and controllers into atomic chunks (e.g. methods)

    • This seems a very good starting point.

    The customization must be possible in the runtime

    I have worked with React.js, Vue.js, Next.js, etc. So, how things are done there is something you can take inspiration from. We have concept of props, hooks, lifecycle events, etc.

    You might also want to draw inspiration from Modern editor systems like CKEditor, Visual Studio Code, or frameworks like Eclipse. Here’s some food for thought:

    Use a plugin registry for each layer (views, controllers, and models). Plugins register extensions/modifications through lifecycle hooks, e.g., onBeforeRender, onAfterRender, onControllerInit, etc.

    Example for ButtonView customization:

    pluginRegistry.register('buttonView', {
        onBeforeRender(buttonInstance) {
            buttonInstance.label = "Custom Label";
            buttonInstance.type = "reset";
        }
    });
    

    Inject dependencies into components via a DI container. You can enable runtime customization by replacing dependencies. For example, allow integrators to replace the default ButtonView implementation:

    diContainer.register('ButtonView', CustomButtonView);
    

    Some more ideas

    • You might want to use a template engine similar to Vue or Angular.
    • middleware pattern from express.js
    • Taking inspiration from UI libraries like MUI, you can craft Extensible Metadata System
      ButtonView.metadata = {
          attributes: {
            type: { default: 'button', editable: true },
            cssClass: { default: '', editable: true },
            label: { default: 'Click Me', editable: true }
          }
      };
      
    • Hooks/Callbacks for Extensibility
      const buttonView = new ButtonView();
      buttonView.hooks.onRender((instance) => {
        instance.label = "Updated Label";
      });
      

    For customizing controllers, you might want to derive inspiration from Vue.js custom event handlers.

    Hope this helps. Happy to discuss more depending on which path suits you.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search