Skip to content

Conversation

@boutell
Copy link
Member

@boutell boutell commented Apr 12, 2024

Demonstrate the custom server side rendering technique. Converts palette settings directly to SASS variables before importing palette.scss, which currently just takes the background color setting and applies it, and also intentionally also contains a rule to make an obvious change to the text color to verify it is in the mix.

@boutell boutell requested a review from carriestreed April 12, 2024 17:57
@linear
Copy link

linear bot commented Apr 12, 2024

PRO-5776 Proof of Concept demonstrating the ability to pass Palette settings through SASS or any preprocessor before generating CSS.

Overview

The head of product design at iCIMS has been inquiring about design token as a potential solution to their needs, and Tom suggests that actually Palette could be enough.

iCIMS's Goal

"Our goal is to allow our customers to brand their site’s content, and we need one codebase (HTML and CSS) for all customers, it seems that this is exactly the path we need to pursue.  I prefer to use Apostrophe as a traditional CMS vs headless, but think we need to stick to “areas” / widget templates will well structured markup, and separate that from the CSS customization and generation."

More specifically, he noted that success would satisfy the following requirements:

  • iCIMS needs to own the CSS solution, relying on Apostrophe for content, structure and markup (HTML templates).

  • Using design tokens as the “what can be customized via fixed markup and fixed CSS” is key to branding and theming.

  • What design token values can be carefully exposed via an intuitive UI to customers should be able to cover 70 – 80 percent of branding requirements

  • The remaining 20 – 30 percent may require more technical knowledge to customize tokens, maybe a different UI

  • 100 percent of the markup and CSS should be the same for all customer brands, relying on the above 2 items to supply the tokens necessary for branding.

Apostrophe's Response & POC Driver

Palette allows developers to configure a schema of fields of various types, and then permits site managers to adjust those fields with immediate visual feedback, therefore it is in essence already a design token editor. Developers can configure the Palette module fields to suit the names of the desired design tokens. They can also organize them visually, with two levels of hierarchy.

While palette does support direct output of CSS properties, the most common strategy today is for Palette to output CSS variables, which by their nature map well to design tokens, allowing you to achieve your goal of a single set of CSS files which specify those CSS variables by name rather than hardcoding particular values for color, border, etc.

You mentioned the possibility of feeding tokens into a SASS compilation pass. SASS is indeed in the mix at the theme level and compiled during deployment, but because all browsers now have support for CSS variables, the job of handling site-level settings can be done more simply and efficiently by utilizing CSS variables for the editable tokens. This allows immediate feedback and avoids the need for per-site SASS compilation.

For efficiency, the palette module compiles the CSS variable settings on the fly when modifications occur, and stages that snippet of CSS in the global document where it can be efficiently accessed on any request.

You mentioned that 20-30% of design tokens might be more complex and require a separate UI. There are several ways we could address that. Some customers have gone with a custom CSS field restricted to certain users, however I hear you when you say you want to avoid any site-specific CSS and stick to the tokens approach. So an alternative, depending on the use cases, could be to leverage Apostrophe’s support for custom field types. I think it would be best to look at specific examples of these complex cases. Perhaps you can share a few?

I’ll also note briefly that since the palette module is based on Apostrophe’s usual features including fields, schemas and REST APIs, it is possible if desired to fetch the palette settings (e.g. the design token values) directly for use in alternative contexts like native apps where CSS variables are not the final implementation.

Regarding the hooks needed:

You can listen for changes in the palette settings in any module like this:

handlers(self) {
  return {
    'apostrophecms/palette:beforeSave': {

      async transformForSass(req, doc) {
        // Do as you see fit here

      }

    }

  };

}

You'd also want to override the "stylesheet" async component of the palette module to output what comes from your SASS rather than the simple CSS compiled directly by palette. The system is designed from the ground up to make overriding things like this straightforward.

One thing to keep in mind however is that the compilation needs to feel fast, as the palette does interactively save often as the user makes changes.

Work to be done

  1. In the palette module move the logic that takes the palette piece and transform it into css into an async method that can be overridden. That function should receive both the schema and the values so it can take schema types and properties into account if desired
  2. Add an option to also invoke that function, via an API call, from the front end without importing SASS into your front end code. When this option is not set (the default) our usual simple renderer is used front and back end for speed
  3. Make sure that method is debounced correctly in the palette module, both when invoked server side and when invoked via the API
  4. You cannot use something like _.debounce function. Possible lodash throttle would be better.
  5. The developer's responsibility is just to write the async function, we handle the debouncing for them.
  6. With that improvement to the Palette module established, create a POC branch in our Assembly Starter Kit in which that method is overridden to pass the values to a SASS style sheet as SASS variables and the CSS output of SASS is what is rendered. The POC will import a specific SASS stylesheet which is not the overall project level public SASS, it is separate and unique for palette purposes.

Notes:

  • If the renderer method throws an error, this will throw an error normally, if we're calling from server side logic it should catch and log and return an empty stylesheet, if we're calling from the API route that wraps it for the front end we should respond with an error. Reasonable fallback styles are the developer's responsibility
  • Document that the developer is responsible for try/catch, returning a reasonable fallback, and making sure their renderer is safe (e.g. you can't type in a string that drops your database in the palette editor)

Acceptance Criteria

  • The POC branch successfully passes the palette values as SASS variables and these are successfully turned into styles
  • This works as expected both when editing live in palette and later when refreshing the page
  • Errors thrown by the rendering function (test this artificially) do not crash the application or the rendered page
  • Debouncing is effective (add artificial delay to your rendering function and verify it does not stack up 1,000 calls as you drag sliders etc)

@boutell boutell changed the base branch from main to v3 April 12, 2024 17:57

module.exports = {
options: {
serverRendered: true
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Necessary to ensure even client-side interactive edits of the palette hit your custom renderer.

},
methods(self) {
return {
getStylesheet(doc) {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your override of this method can be async if it needs to be. I didn't find it necessary for this basic use of the sass compiler.

methods(self) {
return {
getStylesheet(doc) {
const prologue = self.schema.map(field => {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My strategy was to convert every palette field to a sass variable in snake case, and use this to create a prologue passed to sass before importing a palette.scss file.

${prologue}
@import 'palette.scss';
`, {
loadPaths: [ `${__dirname}/scss` ]
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This allows palette.scss to be found and also allows @import in that file using relative paths if desired.

).css;
} catch (e) {
// sass produces highly readable errors this way
console.error(e.toString());
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Show the nice color-coded errors in the server log.

@@ -0,0 +1,7 @@
body {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is clearly a tiny example, but the important thing is that I took a palette field (backgroundColor) and utilized it via its SASS variable equivalent ($background-color).

}

* {
color: purple !important;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know it's obnoxious 😄 I included this rule just to remove any doubt that the custom renderer is in use.

"@apostrophecms-pro/basics": "^1.3.1",
"@apostrophecms-pro/document-versions": "^1.1.0",
"@apostrophecms-pro/palette": "^3.2.0",
"@apostrophecms-pro/palette": "^3.3.0-alpha.1",
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Temporarily necessary. After Wednesday's release you can change this to ^3.3.0 and npm update.

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

Successfully merging this pull request may close these issues.

2 participants