DEV Community

Cover image for Bridging the Gap: Low-Code for Web Developers
Tobias Augenstein
Tobias Augenstein

Posted on • Edited on

Bridging the Gap: Low-Code for Web Developers

Since the early days of my software development career (~15 years ago), I’ve dreamed of a development approach that makes it easy to build and customize professional web applications. Many frameworks have promised to make development simple, but I personally never really found that to be true. While there have been lots of significant improvements (new web standards, TypeScript, SASS, npm, ...) and useful new frameworks, most professional web development still relies on the same languages. TypeScript extends JavaScript, SASS extends CSS and most web frameworks extend JS/TS & HTML. In addition, the setup, deployment and general tech-stack have become more complex. So the overall complexity has become higher, not lower. We keep building more and more on top with limited innovation regarding the fundamental development concepts.

On the other hand there has been a rise of low- and no-code platforms and some of them are really good for certain kinds of use-cases. Unfortunately they usually cause even more headache than web frameworks once you reach the limits of their built-in functionality. But why?
In code we can build core components for frameworks with the same technical concepts as high-level structures, why can’t we do that with low-code concepts? Why does low-code typically fall back to code templates for base components?

I believe it's time to combine the best of both worlds, enabling efficient low-code development based on a well-defined abstraction layer that offers similar flexibility as common web frameworks.


It’s all about trees...

For web UI components the solution is relatively simple. Low-code UI elements are usually defined by a component reference and component-specific attribute configurations. If we support native DOM elements as a special kind of component with generic attributes, we get the full flexibility of HTML directly within the low-code UI structure. We can also combine native DOM elements and higher level components within the same hierarchy. But that doesn't really help much if we still need JavaScript and CSS for logic and styling.

While thinking through more holistic approaches I realized that DOM/UI, scripts/actions and stylesheets have one thing in common - they are all trees. And the nodes of all those trees can be defined based on the following attributes:

  • Node type: Each tree type has its own set of node types defining different core node implementations. All other attributes depend on the chosen node type.
  • Properties: Dynamic attributes for the node’s primary/core implementation.
  • Parameters: Dynamic attributes for a secondary/linked node implementation (e.g. UI component or service method).
  • Events: Actions to be executed based on events triggered by the node.
  • Children: Default slot for child nodes.
  • Child slots: Named slots for child nodes.

With this generic structure we can support a wide variety of tree-based development concepts.


... and JSON

Next we need a technical format for our generic tree structure and luckily that decision is easy: JSON! It’s simple, safe, flexible and very widely supported as the dominant data format of the web.
Before diving deeper into conceptual aspects, let’s look at some examples.

Greet button & service example

Here's a button that calls a custom service method and passes a value from the UI's main data model:

{
  "_ui": "view",
  "view": "forms:button",
  "params": {
    "label": "Say Hello"
  },
  "events": {
    "click": {
      "_action": "service",
      "service": "my-project:user",
      "method": "greet",
      "params": {
        "name": {
          "_bind": "data",
          "ref": "fullName"
        }
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

And this defines a basic implementation for the my-project:user service:

{
  "methods": {
    "greet": {
      "action":  {
        "_action": "script",
        "props": {
          "js": "alert(`Hello ${name}.`)"
        },
        "params": {
          "name": {
            "_bind": "param",
            "ref": "name"
          }
        }
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

But using scripts for high-level logic is exactly what we are trying to avoid with a declarative low-code approach, so here's a more advanced and suitable service example, including validation and more abstraction:

{
  "methods": {
    "greet": {
      "action": {
        "_action": "condition",
        "props": {
          "if": {
            "_bind": "param",
            "ref": "name",
            "modifiers": ["trim"]
          }
        },
        "childSlots": {
          "then": [
            {
              "_action": "service",
              "service": "my-project:dialogs",
              "method": "alert",
              "params": {
                "message": {
                  "_bind": "operation",
                  "operator": "concat",
                  "children": [
                    "Hello ",
                    {
                      "_bind": "param",
                      "ref": "name"
                    },
                    "."
                  ]
                }
              }
            }
          ],
          "else": [
            {
              "_action": "service",
              "service": "my-project:validation",
              "method": "missingRequiredValue",
              "params": {
                "key": {
                  "_bind": "param",
                  "ref": "name",
                  "modifiers": ["key"]
                }
              }
            }
          ]
        }
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Dynamic emoji example

This example defines a native <span> element with a dynamic value binding using a URL hash parameter (e.g. #mode=unicorn). In this case the value property sets the text content of the element, for inputs and other editable elements it provides two-way data binding.

{
  "_ui": "html",
  "props": {
    "element": "span",
    "value": {
      "_bind": "choice",
      "childSlots": {
        "by": {
          "_bind": "url-hash",
          "ref": "mode"
        },
        "cases": {
          "unicorn": "🦄",
          "dragon": "🐲",
          "ocean": "🐠"
        }
      }
    }
  },
  "params": {
    "attr": {}, // Native attribute bindings
    "class": [], // Dynamic class bindings
    "style": {} // Inline style bindings
  }
}
Enter fullscreen mode Exit fullscreen mode

Here you can watch it in action with our IDE prototype:


These JSON trees are easy to parse and modify, by visual editors as well as automation tools. When necessary they are also not difficult to be reviewed and modified manually, e.g. to resolve merge conflicts. It’s also quite simple to build interpreters for actions/services with different back-end programming languages, which makes it a good foundation for technology-agnostic full-stack applications.


Dynamic Stylesheets

One of the most annoying aspects of web development in my opinion are static CSS stylesheets. If you need dynamic styling you either have to pre-define style rules for each case or apply inline style to all involved elements. For many use-cases both options are bad, either for performance or maintainability or even both.

Wouldn’t it be great if we could define dynamic stylesheets? Browsers offer JS APIs for stylesheet manipulation similar to the APIs we use all the time for DOM manipulation. With a dynamic format to define and generate stylesheets we can apply reactive bindings in the same way as reactive DOM bindings.
Let’s look at an example how that can be done with our generic tree structure, using a theme provider service to enable dynamic theme switching.

{
  "services": {
    "theme-provider": {
      "service": "my-project:theme-provider"
    },
    "theme-name": {
      "service": "data",
      "props": {
        "bind": {
          "_bind": "operation",
          "operator": "fallback",
          "children": [
            {
              "_bind": "param",
              "ref": "themeName"
            },
            "main"
          ]
        }
      }
    },
    "theme": {
      "service": "data",
      "props": {
        "bind": {
          "_bind": "action",
          "read": {
            "_action": "service",
            "service": "@theme-provider",
            "method": "getTheme",
            "params": {
              "name": {
                "_bind": "data",
                "ref": "@theme-name"
              }
            }
          }
        }
      }
    }
  },
  "rules": {
    "input-field": {
      "props": {
        "elements": [
          "input",
          "select"
        ]
      },
      "children": [
        {
          "_style": "style",
          "params": {
            "style": {
              "color": {
                "_bind": "data",
                "ref": "@theme.color"
              },
              "border-color": {
                "_bind": "data",
                "ref": "@theme.lightBorderColor"
              },
              "border-width": "1px",
              "border-style": "solid"
            }
          },
          "children": [
            {
              "_style": "style",
              "props": {
                "selectors": [
                  ":hover",
                  ":focus"
                ]
              },
              "params": {
                "style": {
                  "border-color": {
                    "_bind": "data",
                    "ref": "@theme.darkBorderColor"
                  }
                }
              }
            }
          ]
        }
      ]
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

The result is equivalent to the following CSS rules, but adjusts dynamically if any theme value changes at runtime.

input.#{prefix}-input-field,
select.#{prefix}-input-field {
  color: $color;
  border-color: $lightBorderColor;
  border-width: 1px;
  border-style: solid;
}

input.#{prefix}-input-field:hover,
input.#{prefix}-input-field:focus,
select.#{prefix}-input-field:hover,
select.#{prefix}-input-field:focus {
  border-color: $darkBorderColor;
}
Enter fullscreen mode Exit fullscreen mode

Conclusion

Now we can define UI, logic and styling based on a unified dynamic format, which makes it quite easy to provide consistent visual editors and other tooling for all of them. We can also access the same data and services in the same way in all of those contexts. In addition, we can combine different tree types seamlessly within the same tree structure, allowing simple context sharing between them. For example, bindings and event actions can consistently inherit context from their surrounding node, whether it's a UI, action or style node.
And so far we’re only scratching the surface... :)

What are your thoughts? Would you be interested in this kind of low-code development? Where do you see the biggest benefits or challenges?


PS: This article provides a brief introduction to the core concepts of Ontineo. Stay tuned for deeper dives into advanced technical aspects, architecture details of the prototype and more user-focused considerations.

For further information or to follow our project on social media, visit our website: https://ontineo.com

Top comments (0)