Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RFC Silverstripe User Interface (UI) Framework #346

Open
blueo opened this issue Dec 1, 2024 · 21 comments
Open

RFC Silverstripe User Interface (UI) Framework #346

blueo opened this issue Dec 1, 2024 · 21 comments

Comments

@blueo
Copy link

blueo commented Dec 1, 2024

Proposer: @blueo
Contributors: @jakxnz
Area: Silverstripe CMS UI
Posted: December 2nd, 2024
Status: Open

What’s being recommended

Create first-class development patterns for Silverstripe CMS’s UI layer using a micro-frontend and API-first structure

Wanna skip to the best part?

This is a reasonably long RFC. If you want to skip to what you’re interested in you can use these links:

  1. Background (the bit with the rationale/all the text)
    1. Goals
    2. Impacts and considerations
    3. Benefits
  2. What a UI framework could look like (the bit with code examples and suggested changes)
  3. Case study (AKA what it takes to implement UI today)
  4. Other options considered
  5. FAQ

Background

The CMS User Interface (UI) is the primary touch point for content authors and significantly contributes to their impression of the CMS as a whole. Silverstripe CMS has grown through many iterations and the collection of technology and patterns that are used to create UI haven’t been given much attention for some time.

Back in the jQuery days

In CMS version 3 the UI was created by HTML rendered on the server. Interactivity was added with a custom jQuery extension called Entwine. An additional custom library page.js looked after routing, where there is a sprinkling of other third party UI code such as jQuery UI.

React+GraphQL joins the party

CMS version 4 brought in React as a new UI technology along with a GraphQL based API layer. A small number of interfaces (such as the Asset Admin area) were fully converted to a React based interface. More recently there has been a shift away from GraphQL as the main method of delivering data to CMS UI components.

Half-finished migration to new technology

Other parts of the CMS such as the page editor have had piecemeal conversion to React and often still use Silverstripe’s legacy Entwine compatibility layer to render it. There is some documentation on how the React based interface works but it is marked as experimental and it is not comprehensive.

This halfway-house state is detrimental for a number of reasons. Having multiple systems increases the overall complexity and knowledge required to work with and contribute to the CMS UI. The partial implementation coupled with incomplete documentation has created many challenges. For example the case study below shows how many files are required to setup a UI component that works in all parts of the CMS. Overall the lack of direction has a cooling effect on contribution to and innovation within the CMS.

Goals

Our aim is to develop the next generation of the Silverstripe CMS UI into something that brings developing the UI up to a new standard and developer experience. Something that rekindles quick-to-start experimentation, and boosts creative UI improvements/features from Silverstripers and the community to achieve:

  • Reduction of Complexity
    • Make it easier to work with CMS UI by reducing the number and complexity of systems required to create UI components
  • Standardisation
    • Establish a uniform system for creating and customising the CMS UI
    • Use consistent technologies and tools across the CMS to minimise learning and maintenance overhead
    • Ensure that “customisation knowledge” in one part of the CMS is transferable to other areas
    • Replace custom code with standard libraries where applicable.
  • Decoupling and Isolation
    • Allow smaller applications with more flexible parts by reducing interdependencies between parts of the CMS.
    • Minimise the impact of changes (small “blast radius”) to facilitate core and third-party development
    • Ensure community modules can be built without complex dependencies and are helped by clear ways to interface and integrate
  • Extensibility
    • Bring Silverstripe-style extensibility to the CMS UI
    • Provide APIs that allow for multiple levels of UI customisation, similar to PHP's Configuration, Dependency Injection, and Extension APIs
  • Modernisation
    • Utilise modern technologies such as component based JavaScript like React
    • Follow modern patterns such as component-driven development and style isolation
    • Move away from outdated mechanisms like PJAX, ensuring the system is modern and API-driven
  • Re-usable tools
    • A UI framework built for reusability principles, with a foundation that can be used to build a range of web experiences driven by the CMS
  • Documentation
    • Ensure customising the CMS UI is easy to learn and easy to get started, with clear and accessible documentation
    • Provide practical examples that don’t require extensive historical or external knowledge, and wherever possible can be interactive
  • Accessibility and language support
    • Ensure the UI meets the New Zealand Web Accessibility Standard and WCAG 2 requirements
    • Ensure all user-presented language is i18n translatable.

Impacts and considerations

This is great for developers, but isn’t such a “shiny” new feature for non-developers or people looking for a new features in the CMS UX

A UI framework has very developer-centric value and benefits. The benefits to the overall product, such as things like the content editing experience will likely not be felt as a direct impact of a UI framework - In fact, even after a change to the UI framework the content editing experience and other features of the CMS may appear unchanged.

Our assessment suggests that our rate of innovation towards new features is significantly disrupted by the limitations and developer experience of our current UI development tools. Without a quick-to-start and simpler way to customise the UI, important features are not developed, such as easy-to-use advanced features and modules that plugin to 3rd party products.

A UI framework is an architectural change to unblock in-demand features with direct user impact, and time/effort would be needed to develop sought-after features on the new UI framework.

Breaking changes

This will be a breaking change for any current UI code. We expect the developer effort created to upgrade would be mitigated/justified by the value of a simpler way to integrate with the CMS UI and it would be the community’s preference to upgrade to newer features and tools.

We would make sure to consider a forward-compatibility approach using an opt-in model (i.e “feature flags” or “feature toggles”).

Support and maintenance increases

While this would introduce new dependencies, the effect of this would be offset by removing legacy technologies and streamlining into one method of UI implementation.

Better documentation will also help, especially if we follow standards set by documentation we would like to role model such as React, Chainlink and Stripe.

This is very ambitious

Micro-frontends are a relatively new concept. Attempts have been made in the past to modernise the UI in Silverstripe CMS with varying levels of success. There are an awfully large number of UI components that would need to be upgraded to a new UI framework. These include UI components in the CMS core and modules. This recommendation is very ambitious!

We want to use a mature development lifecycle and approach to changes of this scale which is why we have begun with an initial technical assessment, resulting in this RFC.

Engineering challenges

A change of this nature may risk oversimplification (too D.R.Y), as well as other challenges such as navigating conflicts with legacy compatibility.

An incremental improvement approach can be used to help avoid over-engineering, and we can explore opt-in systems, “experimental” releases, and other graceful progression methods to make sure mission critical legacy features are not disrupted, even as they are deprecated over time.

Potential for negative developer experience

There is a risk that developing a component-based architecture, supporting modern components (like stateless and stateful components), unidirectional data flows, hooks, lifecycle methods, federated development structures, new APIs, etc; may be less interesting to parts of the developer community who prefer lightweight architectures and tried-and-true Request/Response or AJAX solutions.

Modern UI frameworks would also increase the entry-level skill required to customise the CMS, and raise the barrier of entry for new adopters.

With all this in mind, modern framework features are a proven requirement from the majority of CMS developers and users in the community. Documentation can be used to assist the skill barrier. There is a use case below which describes how the developer experience would be an improvement on the current Silverstripe CMS UI customisation experience.

We have also observed that the graduate level of skill for web developers leaving online courses or certifications is at the same level that we would want to build a UI framework.

API future-proofing

APIs are hard, and they are easy to design poorly so that they cause breaking changes down the line.

We can use the principles from existing standards and conventions, the guidance of industry-standard API specifications, clear typing and formatting, unit and integration testing, and extensible architectures. These will allow the first-version of a UI framework to be at least as future-proof as Silverstripe CMS’s most matured APIs.

Frontends by their very nature also present a great opportunity to introduce continuous accessibility assurance to our automation CI/CD.

Accessibility standards compliance

Silverstripe CMS’s current level of Accessibility compliance has room for improvement, we would want to make sure future development includes decent quality checks / continuous assurance coverage to create the right conditions for Accessibility to improved as we go.

Documentation and upkeep

The current standard of Documentation could use some TLC and improvement. Done right, a UI framework’s documentation would entail an entirely new module of documentation, and will need to be written to a high standard to achieve the objectives of this RFC.

We don’t have an obvious way to overcome this, but a new feature such as a UI framework is a great driver for us to improve the existing documentation tools to be better for writing great documentation, and also presents an opportunity for new community contribution from those of us looking for less technical ways to contribute.

The community may prefer a new JavaScript library, such as Vue, Angular or Svelte

A technology preference may have emerged in the community for a framework that’s more advanced, simpler/faster, has a stronger community, or has development conventions that are more native to HTTP web applications.

This UI framework can be built in such a way that customisations and extensions can be developed in the JavaScript library/framework of the module developers choice, but the core UI framework will need to be written in one library/framework.

Many of the existing UIs in the CMS are built in React, and can be re-used in a new UI framework. React is still an industry-leader with massive open source support, and is regarded as a good choice for applications with the complexity of something like Silverstripe CMS.

Following the principle of keeping things simple, the recommendation intends to use React as the core UI framework and for Silverstripe CMS upgraded to a new UI framework.

Benefits

  • Ease of use and developer enjoyment
  • Quicker to start developing new features
  • A new way of source controlling, building and composing UI components (aka “micro-frontends” or “federated UI”)
  • Consistency and fewer surprises
  • New knowledge
  • A modern-feeling CMS
  • Inclusivity and accessibility
  • Streamlined maintenance into one modern build
  • Greater compliance and product maturity
  • Stimulate community growth (though developer attraction, collaboration, interest in the product and new CMS adoption)
  • Catalyse more institutional knowledge and community content
  • Motivate more community contribution
  • Remove barriers to more powerful modules and more support for 3rd-party product integration
  • Catalyse big-players to build integrations to their products for Silverstripe CMS
  • Early-mover thought leadership into a micro-frontend CMS UI framework
  • Reinvigorate optimism for the future of Silverstripe

Tip

Lost? Jump back to the document section navigator

What a UI framework could look like

To achieve an extensible, decoupled UI Framework that is well documented with a clear API we are suggesting a micro-frontend based architecture that could look like the following:
image

Note

All APIs in this RFC are intended as illustrations of a concept - actual names and syntax could be different

Composer modules can register Javascript with the standard configuration API to make it available in the front end. This is similar to the Requirements API but acts as a dependency injection container e.g. a third party module could replace a registered module from the core installation:

vendor/silverstripe/demo-input-module/_config/config.yaml:

SilverStripe\Admin\UserInterface:
   modules: 
      demo-input-module:  
        name: 'demo-input-module'
        version: 1
        link: 'silverstripe/demo-input-module:client/dist/boot.js'

A core micro-frontend shell then uses the list of available code via a Feed API and can then load them independently. This allows modules to independently pre-build their assets and the shell to load scripts asynchronously. The Piral Microfrontend framework is an open source tool that can provide this the shell code and feed integration without significant modification.

Creating and organising module code

One of the most painful parts of developing the CMS UI right now is how to get your code onto the page and running in the right place. We recommend having standard way to create module code and a standard API to register code at runtime. This would apply to modules installed via composer and code provided by application developers within the root module.

Module creation

We can leverage Piral’s existing CLI to both simplify creating a module and to quickly setup shared dependencies with the core shell code. This works via the new import maps standard so it does not require complex webpack configuration (or any build tool) to share dependencies.

Runtime

Once loaded, scripts would use a registration API to add features to the CMS UI. Piral has a plugin architecture that allows us to add Silverstripe specific APIs to its setup function. This runs after a module is loaded and allows module code to update the CMS interface. The following is what this API could look like:

vendor/silverstripe/demo-input-module/client/src/boot.js:

// setup called when a module loads - this is where module code interacts with Silverstripe core
export function setup(app) {
  // Theses are example APIs for illustration purposes
  
  // register a notification 
  app.showNotification('Hello from a Silverstripe Module!');

  // register an extension hook to add an item to a list
  app.registerExtensionHook(
    'SilverStripe\Forms\TreeDropdownField'
    'onBeforeMenuDropdown',
    (items) => {
      items.push('<button data-info="data">new action</button>');
    }
  )    
  
  // register a replacement TextField component
  app.registerSilverstripeComponent(
    'SilverStripe\Forms\TextField',
    MyNewTextFieldComponent
  );
  
  // register a new CustomBlock component
  app.registerSilverstripeComponent(
    'App\Blocks\CustomBlock',
    CustomBlockComponent
  );
}

The above example would replace the existing Injector.component.register and updater that looks like:
vendor/silverstripe/demo-input-module/client/src/boot.js:

Injector.transform('tree-search-field', (updater) => {
   updater.component('TreeDropdownField', TreeSearchFieldEnhancer);
});

This is a simplification of the current Injector API which is not widely understood or documented. It may be implemented as a wrapper on the existing Injector, or you could replace it. This would be down to implementers to decide.

Components would be react-based by default, but Piral already includes support for other ways of rendering which would open up compatibility with a wider range of UI technologies.

Extension points

Creating new components is great for large scale UI changes and this is similar to replacing a whole class with the Injector in PHP. Often though developers only need to make small changes to existing code and in PHP Silverstripe provides extension points to achieve this. We propose a first-class Javascript extension API and some principles for when to include extension points in CMS code. The core CMS product should provide extension points in the following scenarios:

  • When rendering a list - it should be possible to add, remove or change items in a list. The API should allow both changing the data and also its presentation (eg which html element is rendered):

    vendor/silverstripe/list-box-module/client/src/components/MyListBox.js

    // example list component that provides an extension point for updating its list
    const MyListBox = ({app, data, onClickHandler}) => {
       const ListItem = app.getSilverstripeComponent('App\Forms\MyListItem');
       
       // get extension function to update the list data
       const extendBefore = app.getExtensionHook('App\Forms\MyListItem.before');  
       const renderData = extendBefore ? extendBefore(data) : data;
       
       const list = renderData.map(item => <ListItem ...item onClick={onClickHandler} /> )
       
       return (
          <ul>{list}</ul>
       );
    }
  • When providing text information - it should be possible to update the wording of relevant text in the UI.

  • When sending or receiving data (e.g. a Fetch call to a REST endpoint) - Data should be able to be altered before it is consumed and before it is sent:

    vendor/silverstripe/list-box-module/client/src/components/AddItemButton.js

    // example button component that has an extension point before sending data
    const AddItemButton = ({app, sendToBackend}) => {
       const itemValue = useItem();
       // get extension function to update the post data
       const extendBefore = app.getExtensionHook('App\Forms\AddItemButton.before');  
       
       const handleSubmit = (event) => {
          event.preventDefault();
          const result = extendBefore(itemValue);
          
          sendToBackend(result);
       }
       
       return (
          <form onSubmit={handleSubmit}>
             <input name="item-1" type="text" value={itemValue}/>
             <button type="submit">Send it 🤙</button>
          </form>
       );
    }
  • In spaces in major UI components - it should be possible to add additional information or related controls in the core UI. This might take the form of a information banner or additional button:

    vendor/silverstripe/extending-module/client/src/boot.js:

    // In extending module
    export function setup(app) {
       app.registerExtensionComponent(
          'App\MyWidget'
          'top',
          () => <Banner type="warning">Some helpful user text</Banner>
       )    
    }

    vendor/silverstripe/extendable-module/client/src/component/MyWidget.js:

    // The Extendable component
    const MyWidget = () => {
       return (
          <section>
             // top component will use content registered in `vendor/silverstripe/extending-module/client/src/boot.js`
             <ExtensionComponent name="App\MyWidget.top" />
             <Carousel/>
             // another available extension component is available below
             <ExtensionComponent name="App\MyWidget.bottom" />
          </section>
       );
    }

UI layout and customisation from the backend

The above does not change how form configuration is managed in PHP code. The getCMSFields and related functions will continue to work and we already have a form schema mechanism for translating this configuration into Javascript.

This combined with script loading and registration means that we can leave the legacy entwine wrapper and injector combination behind while retaining customisation for those not wanting to add more javascript.

Tip

Lost? Jump back to the document section navigator

Case study

To get a feel for what using this new API might be like, this is what creating a new form field (that works in all parts of the CMS) looks like today and how that might change. I’ll assume we’re creating a composer module, but the same principles apply to project code in the root module.

Current state - Creating a custom input field UI

Currently to create a new form field with a custom UI (I’ll call it “DemoInput” for illustration) we can start by defining the field in PHP with a new class in the src/ folder that is a subclass of the core FormField:
vendor/silverstripe/demo-input-module/src/DemoInput.php

<?php
namespace App\Forms;

use SilverStripe\Forms\TextField;

class DemoInput extends TextField
{
    // must match value passed to JS injector below
    protected $schemaComponent = 'DemoInput';
    
}

To setup a custom UI, we’ll need a few things:

  1. A React-based component
  2. A boot file, that registers our component with Silverstripe’s JS injector
  3. A requirement configured for our new component
  4. Updates to pass data from PHP to the Javascript component
  5. An SSViewer template and supporting PHP functions
  6. A wrapper file
  7. A requirement configured for our new wrapper file
  8. Some patience!

Step 1 - React component

We start by creating a React component – This is to ensure the field will work in both the asset admin forms as well as the page edit form. We create the component in the client/src/ folder of the module and can use dependencies via npm etc to create a custom form (eg adding a modal with custom options).

vendor/silverstripe/demo-input-module/client/src/components/DemoInput.js:

import React, { Component } from 'react';
import PropTypes from 'prop-types';
import Modal from 'react-modal';
import styles from './DemoInput.scss';

class DemoInput extends Component {
  constructor(props) {
    super(props);
    this.openModal = this.openModal.bind(this);
    this.state = {modalOpen: false};
  }

  openModal() {
    const { readOnly } = this.props;

    if (readOnly) {
      return null;
    }
    this.setState({openModal: !this.state.openModal});
  }

  render() {
    const { name, value } = this.props;
    return (
      <div>
        <div className={styles.gridContainer} >
          <button
            onClick={this.openModal}
            className="btn btn-info"
            title="Open modal"
          >
            Open Modal
          </button>
          <Modal
            isOpen={this.state.openModal}
            onRequestClose={this.openModal}
            style={customStyles}
            contentLabel="Example Modal"
          >
            Custom content here
            <input type="text" onChange={this.props.onChange} value={value} />
          </Modal>
        </div>
        <input id={name} name={name} type="hidden" value={JSON.stringify(value)} />
      </div>
    );
  }
}

DemoInput.propTypes = {
  onChange: PropTypes.func,
  value: PropTypes.array.isRequired,
  name: PropTypes.string.isRequired,
  readOnly: PropTypes.bool,
};

DemoInput.defaultProps = {
  readOnly: false,
  onChange: () => null,
};

export default DemoInput;

Step 2 - boot file

Once the React component is created we need to register the component with Silvestripe’s JavaScript injector using code like:

vendor/silverstripe/demo-input-module/client/src/boot.js:

import Injector from 'lib/Injector';
import DemoInput from '../components/DemoInput';

document.addEventListener('DOMContentLoaded', () => {
    Injector.component.registerMany({
      DemoInput,
    });
});

The boot file is then added to a webpack config (provided by "@silverstripe/webpack-config") and a build step transforms this into a production bundle in the client/dist/ folder e.g. you need to add to:
vendor/silverstripe/demo-input-module/client/webpack.config.js:

const Path = require('path');
const { JavascriptWebpackConfig } = require('@silverstripe/webpack-config');

const PATHS = {
  ROOT: Path.resolve(),
};

module.exports = [
  new JavascriptWebpackConfig('js', PATHS)
    .setEntry({
     // add boot file to entry configuration
      boot: 'js/boot.js'
    })
    .getConfig(),
];

And the composer.json also needs updating to make sure the file is exposed from the resources endpoint:

vendor/silverstripe/demo-input-module/composer.json:

...
  "extra": {
        "expose": [
            "client/dist"
        ]
    },
...

Step 3 - requirements for boot file

The built boot file file path is then added to a config.yml to be loaded when the CMS page loads:

vendor/silverstripe/demo-input-module/_config/config.yml:

SilverStripe\Admin\LeftAndMain:
  extra_requirements_javascript:
    - 'silverstripe/demo-input-module:client/dist/js/boot.js'

Step 4 - Pass data from PHP to react

To pass data, such as the value of the field from the database or configuration, we need to the following to the DemoInput.php file:

vendor/silverstripe/demo-input-module/src/DemoInput.php:

...
public function getSchemaDataDefaults()
  {
      $schema = array_merge(
          parent::getSchemaDataDefaults(),
          [
              'name' => $this->getName(),
              'value' => $this->Value(),
              'disabled' => $this->isDisabled() || $this->isReadonly(),
          ]
      );

      return $schema;
  }
...

Okay, so we’re part of the way there. The field can now be used in the asset admin by adding it to a Form.

The Form builder will retrieve it from the JavaScript injector and data is passed from the backend to the React component.

Step 5 - SSViewer template and supporting PHP functions

But we’re not quite there yet! To get the component working on the Page edit form, which does not render with React we need an SSViewer template and PHP functions to pass the data into the template:

vendor/silverstripe/demo-input-module/templates/DemoInput.ss:

<div
   <%-- the DemoField class will be used by the JS wrapper below --%>
	class="DemoField<% if $extraClass %> $extraClass<% end_if %>"
	$AttributesHTML('class')
>
  <input id="$ID" type="hidden" name="$Name.ATT" value="$TemplateValue" />
</div>

The PHP Attributes method can then be updated to add any server-side data from step 4:

vendor/silverstripe/demo-input-module/src/DemoInput.php:

...
public function getAttributes()
{
    $attributes = parent::getAttributes();
    $attributes['data-schema'] = json_encode($this->getSchemaData());
    return $attributes;
}
...

Step 6 - Wrapper code

We then need to create an Entwine based wrapper script to load the React component into the SSViewer template. It also passes the values from the template attributes to the rendered component as props and adds some logic for tracking changes made by the React component:

vendor/silverstripe/demo-input-module/client/src/wrapper.js:

/* global window */
import React from 'react';
import { createRoot } from 'react-dom/client';
import { loadComponent } from 'lib/Injector';

window.jQuery.entwine('ss', ($) => {
  $('.js-injector-boot .DemoInput').entwine({
    ReactRoot: null,
    Value: null,

    onmatch() {
      const cmsContent = this.closest('.cms-content').attr('id');
      const context = (cmsContent)
        ? { context: cmsContent }
        : {};
      const DemoInput = loadComponent('DemoInput', context);
      
      const state = this.data('schema') || {};
      this.setValue(state.value ? state.value : {});
      
      const onChange = (value) => {
        this.setValue(value);
        
        // Trigger change detection (see jquery.changetracker.js)
        clearTimeout(this.getTimer());
        const timer = setTimeout(() => {
          form.trigger('change');
        }, 0);
        this.setTimer(timer);
      };

      let root = this.getReactRoot();
      if (!root) {
        root = createRoot(this[0]);
        this.setReactRoot(root);
      }
      root.render(
        <DemoInput
          {...state}
          onChange={onChange}
          value={this.getValue()}
          noHolder
        />
      );
    },

    onunmatch() {
      const root = this.getReactRoot();
      if (root) {
        root.unmount();
        this.setReactRoot(null);
      }
    }
  });
});

This file also needs to be added to the webpack config e.g.

vendor/silverstripe/demo-input-module/client/webpack.config.js:

const Path = require('path');
const { JavascriptWebpackConfig } = require('@silverstripe/webpack-config');

const PATHS = {
  ROOT: Path.resolve(),
};

module.exports = [
  new JavascriptWebpackConfig('js', PATHS)
    .setEntry({
      boot: 'js/boot.js',
      // add wrapper file to entry configuration
      wrapper: 'js/wrapper.js'
    })
    .getConfig(),
];

Lastly, the entwine wrapper file needs to be loaded when the Page editor loads and we can achieve this in PHP with:
vendor/silverstripe/demo-input-module/src/DemoInput.php:

public function Field($properties = [])
  {
      // Include react bootstrapper
      Requirements::javascript('silverstripe/demo-input-module:client/dist/js/wrapper.js');

      return parent::Field($properties);
  }

Phew! Okay. So now jQuery/Entwine should use the identifier from our SSView template to render our React component. Props are passed from our HTML attributes to our React component. jQuery/Entwine links our React code to its execution using an on-change handler, and create a hidden input to save the field’s value into. When the form is submitted, it is this hidden input field that passes form data entered into our DemoInput to the back-end.

At last! We have a working DemoInput using a custom UI.

Some boilerplate repositories exist (eg GitHub - maxime-rainville/silverstripe-react: Helper library for creating react components in Silverstripe CMS ) to help with this process but it still has a lot of moving parts.

Proposed state

The proposed state is simpler! And with some key differences would remove steps 5 -7 entirely while streamlining the remaining process:

  • Custom UIs would still be distributed as modules using composer.json and developers would composer require community Custom UI modules
  • A custom component itself would be created the same way
  • Less boilerplate would be needed
    • Only one file would be needed to boot and register the custom component
    • No bloat from SSViewer templates or associated PHP functions would be needed
    • No jQuery based wrapper script would be needed
    • Developers would not need to update JavaScript Requirements via PHP (see below)
  • Developers would use an independent build toolchain, and developers would not be forced to use the CMS framework’s full build toolchain just for a custom UI (this also means simple custom UIs may not need a build toolchain at all)
    • Making customisations in isolation like this would be more consistent with how customisations can be made in PHP (e.g with one extension file and one configuration to register it)
  • Developers could use a single command via CLI to generate boilerplate files
  • Loading the custom UI into the DOM and passing configuration data to the component would be simpler, using only a JS Load API
  • Components would have the ability to define their own extension hooks
  • Custom components can be written in a range of JavaScript libraries, such as vanilla JavaScript, React, Preact, Vue, LitElement, Svelte and Angular

Other options considered

  • Switching to Vue or Svelte (or insert framework here)

    We avoided this option because using a new framework would amount to a full rewrite of the React code which would make the effort unreasonably high. Component driven frameworks have their different approaches but are ultimately fairly similar so we don’t feel there is a compelling reason big enough to justify the effort

  • Remove complex JavaScript libraries like React and go back to simple jQuery

    Sure, jQuery is simple to use especially for small changes and so it is rightfully a popular option. There are two reasons why we don’t think it suits the future of the CMS. The first is that jQuery code becomes hard to manage in larger applications. The CMS is essentially an application for content creators with many parts and use cases. Component driven JavaScript was born from a need to provide better structure and organisation to larger JavaScript codebases, and the tooling and patterns they provide are now industry standard. Secondly innovation and future development in the JavaScript eco-system is heading away from jQuery. It is becoming more difficult to find jQuery plugins, especially for integrations. New ideas do not often target the jQuery space

  • Server controlled HTML (HTMX/stimulus)

    The HTML-first option is an interesting one popularised by the likes of BaseCamp. Moving to such a system would be a large paradigm change for the CMS UI. We avoided this option because it would generate significantly more effort in technical changes, documentation, and developer on-boarding. There are also some risks in terms of performance (on the server side) and the level of interactivity available with an HTML-first approach. Our experience is also that the load that the CMS application can support is lower than what we anticipate would be needed to support server-controlled UI requests. Taken together we think the shorter path is to start with what we already have and make the best of it

  • Do nothing

    As mentioned in the introduction the current half-way state is not serving anyone well and makes developing with Silverstripe difficult. We believe something must change to set a clear direction for development and unblock innovation in the CMS (and the fun of using the Silverstripe)

Tip

Lost? Jump back to the document section navigator

FAQ

1. Why micro-frontends?

A microfrontend framework such as piral gives us the ability to organise, streamline and decouple UI components. The JS Module API allows simplified loading of UI code from a range of modules and the setup and registration patterns help to separate concerns and provide organisation.

Microfrontends were originally designed for multiple teams working on a single application (eg Homepage – V2 ). In this situation an application needs to retain consistency but allow different teams to work independently. This is very similar to our CMS where we want to create a cohesive application that multiple groups can work on and contribute to without tripping over each other.

2. Why are you choosing to stay with React?

Many of the existing UIs in the CMS are built in React, and can be re-used in a new UI framework.

React is still an industry-leader with massive open source support, and is regarded as a good choice for applications with the complexity of something like Silverstripe CMS.

Following the principle of keeping things simple, staying with React would be the best option.

3. Would a new UI framework support Headless?

Yes. The API-first approach will make it easier to support headless use-cases with more available from a REST like endpoint.

4. Could I write custom UI components in my favourite JavaScript framework, like Vue, Angular or Svelte?

Yes. This UI framework can be built in such a way that customisations and extensions can be developed in the JavaScript library/framework of the module developers choice, but the core UI framework would be written in React.

Check out Piral’s FAQ for a range of examples of how other frameworks/patterns can be used: https://docs.piral.io/faq/plugins

5. Would we get rid of Webpack?

Yes and no. A microfrontend architecture means each component can use its own build system. Much of the CMS is built using webpack so for practicality we will likely continue to use webpack for the core modules.

TLDR; Webpack will still be around but it won’t be required when building for the CMS and integrating with it.

6. What would stop this from becoming yet another halfway-house state?

There is a risk we start down a path and never finish but that is the same for any larger change. Keeping focus and priorities are, as usual, the only things keeping change from progressing completely.

7. What will happen to GridField?

We expect GridField will work well with this new UI framework. With more and richer REST endpoints we see Gridfield becoming even more powerful. If we design it right then it opens up the possibility of leveraging open source libraries such as Griddle to deliver customisable data views while keeping the familiar PHP interface. Ideally this will reduce our maintenance burden while keeping the the developer friendly interface we all know and love.

8. What will happen to Elemental?

Elemental will likely only need minimal changes to work with the new framework as its interface is largely React-driven already. Longer term the extensibility features are designed to open up Elementals interface to be more customisable.

9. Will debug bar work with a new UI framework?

Debug bar provides its own UI which is an overlay on the CMS so it will largely continue to work as is though it may need to load its own jQuery library in the CMS area. There would no longer be as much template information for the CMS forms. Other tools such as React’s Dev tools can provide similar information.

10. Does this mean you want to get rid of GraphQL?

No. While the CMS team is not focussing on GraphQL to deliver features, GraphQL will continue to be an option for those wanting to use it.

11. Will there be a RESTful API?

Endpoints in the CMS will continue to use standard Silverstripe CMS controller endpoints, which are added as needed.

12. Would the UI frameworks extension points be named the same as the PHP frameworks? e.g. updateMethodName / onBeforeMethodName /onAfterMethodName?

Where it makes sense to yes! Ideally the experience of extending the CMS UI is familiar to backend developers who use Silverstripe’s PHP extensibility. If using the same or similar names becomes confusing we’re open to creating a new convention.

13. Would there be a Marketplace of UI components?

No. Silverstripe CMS would still use the existing ways to search for and find modules to install and use.

14. Would I need to learn to code in a new framework?

Kind of. Learning the UI framework would be a lot like learning the Silverstripe CMS Configuration API or ORM.

15. Will micro-frontends come with ways to develop them using DevOps?

Yes. Microfrontends allow more isolated build and development so they should fit nicely into any DevOps pipeline or workflow.

16. You haven’t mentioned Redux - is that still relevant?

Under the hood. Redux is used by parts of the CMS interface for shared state such as form state. Wherever possible we would like to avoid sharing state as it creates difficult dependency scenarios and less portable components. Ideally UI components stay self-contained and can mange their own state using whatever flavour of management they like. Where components need to share state it should be provided by a clear interface.

17. Do we need to keep JavaScript Injector for backwards compatibility?

Most likely it will live on in some form for both backwards compatibility and its feature set. Much of the JavaScript Injector is well thought out and useful but it is only sparsely documented which makes it difficult to use.

Thanks!

Thank you for reading through and giving this RFC your consideration. Please add your thoughts to the comments or add a reaction.

Tip

Lost? Jump back to the document section navigator

@jakxnz jakxnz changed the title RFC Silverstripe User Interface Framework RFC Silverstripe User Interface (UI) Framework Dec 2, 2024
@kinglozzer
Copy link
Member

There’s a lot to digest and consider so I’ll come back later with some thoughts, just wanted to say thanks @blueo & @jakxnz (and anyone else who contributed!) for taking this on and for writing it up so coherently.

@andrewandante
Copy link

@blueo and @jakxnz love this, an excellent write-up and thank you so much for laying out all your reasoning and considerations!

One thing that's not quite laid out that I'd love some more information on is where the option "Just finish the React switchover" sits. I feel like this approach suggests that we'd need to rip out a few other pieces (entwine etc) before we could do that, and so leaning into this micro-frontend paradigm is a way of both a) getting people excited about the transition and b) outlining what "success" looks like so that we can re-start the engines - is that right, or is there some additional context?

Great work!

@GuySartorelli
Copy link
Member

GuySartorelli commented Dec 3, 2024

One thing that's not quite laid out that I'd love some more information on is where the option "Just finish the React switchover" sits

In many ways, this proposal represents that option - but instead of just finishing the switchover with the current react paradigm, it aims to finish it while making it easier to work with.

It's no secret that the react injector is hard to work with, and that the sections that use react without entwine right now are hard to customise. Rather than just taking away entwine and putting more hard-to-customise react in its place, this proposal aims to build an extensible and customisable framework into which the react switchover will sit nicely.

Removing entwine will obviously break a lot of customisations, since most CMS sections right now rely on it. So the goal is to ensure that when we replace entwine with react in the sections that still use it, those customisations can be recreated in a hopefully intuitive way, rather than fighting against the react injector and giving up.

At least that has been my understanding.

The introduction of the new framework could theoretically be done prior to the full switchover (with a number of caveats).... a release schedule for this would need to be planned out carefully. I'm personally in favour of having this be a major focus of CMS 7 rather than piecemeal implementation in minor releases in CMS 6.

@blueo
Copy link
Author

blueo commented Dec 3, 2024

One thing that's not quite laid out that I'd love some more information on is where the option "Just finish the React switchover" sits. I feel like this approach suggests that we'd need to rip out a few other pieces (entwine etc) before we could do that, and so leaning into this micro-frontend paradigm is a way of both a) getting people excited about the transition and b) outlining what "success" looks like so that we can re-start the engines - is that right, or is there some additional context?

Thanks @andrewandante you're right the idea is really an extension of "finish the switchover" but with some ideas I've been sitting on about what 'success' looks like (@GuySartorelli's first sentence puts it much better tbh). So yep it would require removing jQuery/entwine parts. Hopefully that's what you were asking - happy to expand on anything if not!

@andrewandante
Copy link

Perfect, that's exactly what I wanted to clarify and was my initial hunch, cheers both!

@forsdahl
Copy link

forsdahl commented Dec 3, 2024

@blueo @jakxnz well done RFC in my opinion. I like the idea of micro frontends using Piral. We are quite into Svelte here on our end, but it seems Piral already has a plugin for Svelte, https://docs.piral.io/plugins/piral-svelte, so quickly looking it seems that it would also be covered. For us, having the option to just override parts of the UI with our own components, in our own preferred js framework, is really appealing.
One tricky trade-off is going to be how many extension points to do in core components, without making the core component code too abstract and hard to maintain/follow, but still add enough extensibility so that developers very seldom need to override complete components. If a developer needs to override a complete component, using any other framework than React will mean they have to re-implement the whole component. Even using React, extending an existing component is usually tricky, if you can't just use a higher-order component.

@chrispenny
Copy link

Wow, what an epic RFC!

For my part, I'm not sure I have anything to contribute to the conversation, other than to say that the Embargo & Expiry module has long wanted to improve its UI, but I've so often given up due to the difficulty in working with React in the CMS. I'll also say that it was really interesting to read about the micro frontends approach and Piral.

Great work, team!

@mateusz
Copy link

mateusz commented Dec 3, 2024

I'm not active in the community, but since @blueo asked to read this through 😄 I like the proposal, primarily because it's a good vehicle to finish off the JS migration. I think it's a fantastic motion to standardise on a single framework (whatever it is).

On React, it can be hard, but it offers plenty of stability (technically and career-wise), so it is "not wrong" to stick with it.

Lots of steps and boilerplate to create JS components wouldn't bother me, but trying to weave all JS in a project into a single build pipeline can be pretty annoying operationally. Having modules ship with compiled components (at least that's how I understand this proposal?) makes it super easy to install stuff.

On allowing people to use their own JS frameworks in modules, I don't have an opinion 😁 But one angle I have is it's not a big deal if each section or control has it's own UI little quirks - e.g. AWS Console is like that and it's not a big deal.

Which brings me to the one concern I could think up - is investing in Piral a smart choice? Would it turn into 2030 version of entwine🗡️? I'm not a JS pro, but would be interesting to know if there is perhaps a more standard/idiomatic way to achieve microfrontend architecture in React world?

@jakxnz
Copy link

jakxnz commented Dec 3, 2024

It's not a big deal if each section or control has it's own UI little quirks - e.g. AWS Console is like that and it's not a big deal.

That's a really good consideration.

Also great question there @mateusz 🤔

@blueo
Copy link
Author

blueo commented Dec 3, 2024

Thanks @forsdahl I've updated the comment on svelte. Good observations re core components + extensibility.

One tricky trade-off is going to be how many extension points to do in core components, without making the core component code too abstract and hard to maintain/follow, but still add enough extensibility so that developers very seldom need to override complete components.

Yes this will need to be a balance. I imagine it would progress in a similar way to how it does on PHP - a common PR is a to add an extension point for a place where you'd like some control. The above 'guidelines' for adding extension points will hopefully mean a good base level for new components without getting to abstract but it is certainly something to watch out for.

If a developer needs to override a complete component, using any other framework than React will mean they have to re-implement the whole component. Even using React, extending an existing component is usually tricky, if you can't just use a higher-order component.

My aim would be that overriding a complete component becomes a last resort - and even doing this in React is largely a re-implementation. I kinda think of this as similar using Injector in PHP to provide a new implementation of a class. I think having the ability to insert non-react components within a core component would help here. the <ExtensionComponent name="App\MyWidget.top" /> would accept things via piral plugins. Along with adding 'hooks' this would ideally make it similar to adding a PHP extension and give more access into a component than currently available

@emteknetnz
Copy link
Member

@silverstripe/core-team you may have some opinions to share on this RFC

@GuySartorelli
Copy link
Member

GuySartorelli commented Dec 3, 2024

I think having the ability to insert non-react components within a core component would help here. (@blueo)

A lot of - probably most of - the components used will be/are form fields. The current react form setup in Silverstripe CMS requires using redux, which I think is explicitly for react.

All this to say: Would non-react components work as replacements for react-based form field components?

@blueo
Copy link
Author

blueo commented Dec 3, 2024

thanks @mateusz for giving this a read!

Which brings me to the one concern I could think up - is investing in Piral a smart choice? Would it turn into 2030 version of entwine🗡️? I'm not a JS pro, but would be interesting to know if there is perhaps a more standard/idiomatic way to achieve microfrontend architecture in React world?

The 'big players' in the microfrontend framework space appear to be SingleSPA, Bit and piral (with a few other such as open components and Qiankun. Other approaches are more low-level such as Webpack module federation and SystemJS which is used under the hood by both Piral and SingleSPA. All of them aim to allow multiple technologies to play nicely together as they're aimed at places with multiple teams so they're not react specific. I think the exact selection of library doesn't have to be decided here more the concept. Having said that, Piral brings some nice structure to load components via an API and a well defined plugin system. Its not just supported by us - which I think would help prevent it becoming entwine.

@blueo
Copy link
Author

blueo commented Dec 3, 2024

A lot of - probably most of - the components used will be/are form fields. The current react form setup in Silverstripe CMS requires using redux, which I think is explicitly for react.

All this to say: Would non-react components work as replacements for react-based form field components?

Redux is designed to be framework agnostic so in theory it will work just fine with other javascript. Having said that I think we should avoid exposing too much Redux/shared state directly - if we had our own layer between it could provide a bit of an abstraction so that the core is able to make changes internally and components have a stable 'target' to develop to.

I checked with piral and the converter plugins do allow passing data between react/non react. Eg using the piral-vue plugin you could do something like:

app.registerComponent('App\DemoInput', fromVue(MyVueInput));

And the onChange handler/data values would be passed to the vue component for use there. Alternatively, we could provide some state/form apis via the app object that gets passed to the setup function.

@dannidickson
Copy link

I don't think I will have much to contribute to the discussion. Wanted pop in and say I appreciate the time and effort that has gone into this RFC. Very well thought out and clearly outlines a path forward.

Something like this is a massive undertaking and may have growing pains in the short term. Though I think it is a worthwhile investment, as this will unlock a lot of new possibilities within the CMS. Being able to "just" extend a JS component like we do with Silverstripe templates... I look forward to the day.

This may have been answered in the RFC already, but would this UI framework be decoupled as its own module and/or npm package?

@blueo
Copy link
Author

blueo commented Dec 3, 2024

This may have been answered in the RFC already, but would this UI framework be decoupled as its own module and/or npm package?

Hmm good question - I'd love it to be re-usable across contexts so from that point of view a package would be good. Might come down to implementation preferences/ease of distribution.

@GuySartorelli
Copy link
Member

GuySartorelli commented Dec 4, 2024

This may have been answered in the RFC already, but would this UI framework be decoupled as its own module and/or npm package?

There has been some discussion internally of having it available for use in the front-end - one way to enable that use case is to have it as a module which is included as a dependency of silverstripe/admin. But like Bernie says, that's an implementation detail that isn't covered by this RFC.

@sminnee
Copy link
Member

sminnee commented Dec 4, 2024

Scoped as "here's how we finish off the React migration and provide a better development experience", it seems like a worthwhile thing to explore.

I would caution, however, against adding too many non-essential goals, however, such as supporting multiple front-end frameworks. Migrating the non-React stuff to React makes sense.

I would also say that, while work on micro-frontend may provide some useful lessons / tools, focus on the specific problem that needs to be solved (elegantly combining javascript from multiple composer module) rather than "Going Micro-frontend" as an end-in-itself.

For example, Piral's appropriateness should be very-much judged by how well it allows for bundling of JavaScript into composer packages.

Related: how much will be simplified by dropping the need for entwine out of the page editor?

@blueo
Copy link
Author

blueo commented Dec 4, 2024

Scoped as "here's how we finish off the React migration and provide a better development experience", it seems like a worthwhile thing to explore.

I would caution, however, against adding too many non-essential goals, however, such as supporting multiple front-end frameworks. Migrating the non-React stuff to React makes sense.

I would also say that, while work on micro-frontend may provide some useful lessons / tools, focus on the specific problem that needs to be solved (elegantly combining javascript from multiple composer module) rather than "Going Micro-frontend" as an end-in-itself.

For example, Piral's appropriateness should be very-much judged by how well it allows for bundling of JavaScript into composer packages.

Related: how much will be simplified by dropping the need for entwine out of the page editor?

Yeah additional frameworks is more of an opportunity that it would allow and not the main aim. Having said this it anecdotally seems a popular idea.

Agreed microfrontend is not the goal - its an easy to develop/add/extend and flexible UI with a clear API - I think the microfrontend pattern fits this well but if it turns out to be a real pain then providing a custom api would be a fallback. I would say though that the FeedAPI, import maps + system JS sorts out a bunch of module loading and dependency sharing issues and this is nicely packaged in something like piral.

As for simplification - I think targeting one set of tools/patterns will be a lot simpler. rather than writing things in a hybrid way with extra wrapper files - or having to choose which bits of the CMS your code works with is a real problem. Entwine is a part of the issue but its more having to support it along with jQuery, bootstrap, jQuery UI, some custom plugins and the template system - on top of react.

@sminnee
Copy link
Member

sminnee commented Dec 5, 2024

I would say though that the FeedAPI, import maps + system JS sorts out a bunch of module loading and dependency sharing issues and this is nicely packaged in something like piral

Yeah that sounds really pragmatic 👍

Entwine is a part of the issue but its more having to support it along with jQuery, bootstrap, jQuery UI, some custom plugins and the template system - on top of react.

Sorry, yeah, by "entwine" I meant "the legacy stack, which is based on entwine"

@mfendeksilverstripe
Copy link

Really exciting changes 🎉 . I'm especially happy about the in-depth understanding of the extensibility challenges and also that GridField is expected to get some love too ❤️ . Looks like the step in the right direction 👍.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests