# Exclaim - [What and Why](#what-and-why) * [Design Goals](#design-goals) * [Differences from Ember Exclaim](#differences-from-ember-exclaim) - [Installation](#installation) - [Usage](#usage) * [Configuration](#configuration) * [Creating an Exclaim::Ui](#creating-an-exclaimui) * [Implementing Components and Helpers](#implementing-components-and-helpers) + [Basic Examples](#basic-examples) + [Defining the Implementation Map](#defining-the-implementation-map) + [Child Components](#child-components) + [Variable Environments](#variable-environments) + [Shorthand Properties and Configuration Defaults](#shorthand-properties-and-configuration-defaults) + [Security Considerations](#security-considerations) - [Script Injection](#script-injection) - [Disable HTML escaping](#disable-html-escaping) - [Unintended Tracking/HTTP Requests](#unintended-trackinghttp-requests) * [Querying the Parsed UI](#querying-the-parsed-ui) * [Utilities](#utilities) - [Development](#development) - [Contributing](#contributing) - [License](#license) ## What and Why Exclaim is a JSON format to declaratively specify a UI. The JSON includes references to named UI components. You supply the Ruby implementations of these components. For example, here is an Exclaim declaration of a `text` component: ``` { "$component": "text, "content": "Hello, world!" } ``` Your implementation of this `text` component could simply echo the configured `content` value: ``` ->(config, env) { config['content'] } ``` The above would render the plain string `Hello, world!` Alternatively, your implementation could wrap the `content` in an HTML `span` tag: ``` ->(config, env) { "#{config['content']}" } ``` Then rendering the UI would produce `Hello, world!` Similarly, you could implement an `image` component to replicate an HTML `img` tag: ``` ->(config, env) { "" } ``` and declare it in JSON like so: ``` { "$component": "image, "source": "/picture.jpg", "alt": "My Picture" } ``` These `text` and `image` components are just examples - Exclaim does not require implementing any specific components. The needs of your domain determine the mix of components to implement. By implementing more complex components, including ones that accept nested child components, you prepare the building blocks to specify a full UI. Then, this library will accept JSON values representing arbitrary UIs composed of those component references, and call your implementations to render them. ### Design Goals Exclaim has several high-level goals: * Enable people to declare semi-arbitrary UIs, especially people who do not have direct access to application code. * Support variable references within these UI declarations. * Provide the ability to offer custom, domain-specific UI components, i.e. more than what standard HTML provides. * Represent UI declarations in a data format that is relatively easy to parse and manipulate programmatically. * Constrain UI declarations to help avoid the XSS/CSRF vulnerabilities and automatic URL loading built into HTML. Exclaim component implementations still must handle these issues (see [Security Considerations](#security-considerations)), but JSON provides an easier starting point. Other good solutions exist that fulfill slightly different needs. * [HTML](https://developer.mozilla.org/en-US/docs/Web/HTML) itself enables declarative UIs, of course, and with adequate input sanitization, a platform could host HTML authored by end users. * Templating languages like [Handlebars](https://handlebarsjs.com/) or [Liquid](https://shopify.github.io/liquid/) add variables and data transformation helpers. * For a developer building an interactive web application, it would be more straightforward to use any standard JavaScript framework, such as [Ember](https://emberjs.com/). * The [Dhall](https://dhall-lang.org/) configuration language enables safe evaluation of third-party-defined templates and functions, and has a similar spirit to Exclaim, although it does not use JSON as its source format. ### Differences from Ember Exclaim Salsify's [Ember Exclaim](https://github.com/salsify/ember-exclaim) JavaScript package originated the format, and this Ruby gem aims to work compatibly with it, aside from intentional differences described below. Ember Exclaim puts more emphasis on providing interactive UI components. It leverages Ember [Components](https://api.emberjs.com/ember/release/classes/Component) to back the Exclaim components referenced in the JSON, and Ember Components expressly exist to render HTML that dynamically reacts to user actions. In both JavaScript and Ruby, Exclaim components render in the context of a bound data environment, but Ember Exclaim sets up two-way data binding for the components, where user input automatically flows back into the UI's environment. In contrast, the Ruby side focuses on one-way rendering, with more emphasis on bulk rendering a UI for multiple data environments. For example, at Salsify a key data entity is a product, and this library could take a customer's UI configuration to display info about a product and render it for each of many products (data environments). Furthermore, this gem omits several features of [Ember Exclaim](https://github.com/salsify/ember-exclaim): * It does not implement `resolveFieldMeta`, `metaForField`, or `resolveMeta`. These features are secondary to Exclaim's core functionality. * It does not support `onChange` actions, which are more relevant for interactive components. * It does not accept a `wrapper` component to wrap every declared component in a UI configuration, as this is rarely required. Please reach out if you have a concrete need for these features in Ruby. ## Installation Add this line to your application's Gemfile: ```ruby gem 'ruby-exclaim' ``` And then execute: $ bundle Or install it yourself as: $ gem install ruby-exclaim ## Usage ### Configuration The only configuration option is the logger, which expects an interface compatible with the standard Ruby [`Logger`](https://ruby-doc.org/stdlib/libdoc/logger/rdoc/Logger.html). In Rails, it will default it to `Rails.logger`. ``` Exclaim.configure do |config| config.logger = Logger.new($stdout) end ``` ### Creating an Exclaim::Ui We will cover how to implement components shortly. For now, assume that you have a simple `text` component implementation, and an _implementation map_ containing it: ```` text_component = ->(config, env) { config['content'] } my_implementation_map = { "text" => text_component } ```` First, instantiate an `Exclaim::Ui`: ``` exclaim_ui = Exclaim::Ui.new(implementation_map: my_implementation_map) ``` Then, assume that you have a JSON UI configuration referencing the `text` component: ``` { "$component": "text", "content": "Hello, world!" } ``` This JSON could be stored in your DB, fetched from a web API, or supplied any other way. To use it with this library, the JSON must be parsed into a Ruby Hash. Note that the hash keys must remain as type String. ``` my_ui_config = { "$component" => "text, "content" => "Hello, world!" } ``` Call the `parse_ui!` method to ingest the UI declaration, preparing it for rendering: ``` exclaim_ui.parse_ui!(my_ui_config) ``` Finally, call the `render` method to render the UI: ``` exclaim_ui.render => "Hello, world!" ``` The UI JSON may include `$bind` references, which act like variables: ``` { "$component": "text, "content": { "$bind": "greeting" } } ``` This will render with a Hash of values supplied as the _environment_ (usually abbreviated as `env`): ``` my_environment = { "greeting" => "Good morning, world!" } exclaim_ui.render(env: my_environment) => "Good morning, world!" ``` Dot-separated `$bind` paths dig into nested `env` values: `a.b.c` refers to `{ "a" => { "b" => { "c" => "value" } } }` If the field a `$bind` subpath refers is an Array, the next segment is assumed to be an integer. For example, `"my_array.1"` refers to array index 1, value "zero" in an `env` like `{ "my_array: ["zero", "one", ...] }`. ### Implementing Components and Helpers Note that implementations have __important [Security Considerations](#security-considerations)__. Component implementations typically return HTML Strings. As desired, you can leverage a Ruby templating tool like [ERB](https://rubygems.org/gems/erb) to do this, but simple string interpolation works too. Rendering HTML is the primary purpose of Exclaim, and in situations when you want the UI configurations to work interchangeably in Ember and Ruby, the Ruby component implementations will need to produce equivalent HTML to the Ember components. However, Ruby components technically do not _need_ to return HTML Strings. They could return some other Ruby value, like a Hash representing the JSON payload to submit to some API. In addition to components, Exclaim also has helpers. The distinction between components and helpers is stronger in Ember Exclaim, since there components are Ember [Components](https://api.emberjs.com/ember/release/classes/Component), while helpers are plain JavaScript functions. Nevertheless, helpers have the same spirit in the Ruby version: They do not render output directly, but instead to transform data supplied as component configuration. As an example, suppose you define a `coalesce` helper intended to extract the first non-nil value available from an Array. It would support UI declarations like below, where the `text` component's `content` configuration becomes a dynamic `pizza_topping` value from the `env`, if present, or falls back to `"plain cheese"`: ``` { "$component": "text", "content": { "$helper": "coalesce", "candidates": [{ "$bind": "pizza_topping" }, "plain cheese"] } } ``` The following implementation could back this `coalesce` helper: ``` ->(config, env) { config['candidates'].compact.first } ``` In Ruby Exclaim, both component and helper implementations are objects that respond to `call`, such as a [lambda or proc](https://ruby-doc.org/core-3.0.0/Proc.html), [`Method`](https://ruby-doc.org/core-3.0.0/Method.html) object, or instance of a custom class which defines a `call` method. More precisely, implementations: * Must provide a `call` interface. * The `call` interface must accept two positional parameters, `config` and `env`, both Hashes. * Component implementations can optionally accept a block parameter, `&render_child`, which the implementation can use to render child components specified in its config. That does not apply to helper implementations. In addition, these implementations must define either a `component?` or `helper?` predicate method. These must return a truthy or falsy value to identify their type. #### Basic Examples See also the `lib/exclaim/implementations` directory for more code examples, and `spec/integration_spec.rb` to see them in action. Returning to the `text` component mentioned above, we could implement it a few different ways. A lambda: ``` text_component = ->(config, env) { config['content'] } text_component.define_singleton_method(:component?) { true } ``` Or a custom class: ``` class Text def call(config, env) config['content'] end def component? true end end text_component = Text.new ``` If needed, a different `call`-able, such as a block or Method object: ``` def generate_implementation(is_component:, &implementation_block) implementation_block.define_singleton_method(:component?) { is_component } implementation_block end text_component = generate_implementation(is_component: true) do |config, env| config['content'] end ``` Helpers are very similar: ``` # lambda join_helper = ->(config, env) { config['items'].to_a.join(config['separator']) } join_helper.define_singleton_method(:helper?) { true } # class class Join def call(config, env) config['items'].to_a.join(config['separator']) end def helper? true end end join_helper = Join.new ``` Implementations may define both `component?` and `helper?`, as long as they have opposite truth-values. They only need to define one of them, though, since one implies the converse value for the other. #### Defining the Implementation Map With some components and helpers implemented, an application should put them in an _implementation map_ Hash. ``` IMPLEMENTATION_MAP = { "text" => text_component, "vbox" => vbox_component, "list" => list_component, "coalesce" => coalesce_helper "join" => join_helper } ``` Pass it in when instantiating an `Exclaim::Ui`: ``` exclaim_ui = Exclaim::Ui.new(implementation_map: IMPLEMENTATION_MAP) ``` This library comes with several element implementations, collected into an example implementation map. You can freely use some or all of them, but there is no requirement to do so. A basic assumption of Exclaim is that client code will provide a custom mix of components. Many applications will only need a single, application-wide implementation map, but it is quite possible to define more than one, passing them into different `Exclaim::Ui` instances. Example reasons why an application might define multiple implementation maps: * One set of implementations to render HTML for public consumption, another that draws highlights around elements for internal reviewers. * You have two target websites that need dramatically different HTML organization or CSS classes. * You want to implement multiple `brand_container` components that embed parallel stylesheets and logos. * One set of implementations that renders HTML, another to render JSON payloads for an API. * A set of implementations that should only be used with trusted UI configuration/environment values, and another more constrained set to use with untrusted values. Another way to accomplish the goals above would be to put conditional logic in the implementations, and passing variable `env` Hashes to drive it when rendering. The right strategy depends on the amount of variation and how you want to organize your implementations. #### Child Components Components can have nested child components, where the parent incorporates the rendered child values into its own rendered output. Consider a `vbox` component which renders its children in a vertically oriented `div`: ``` { "$component": "vbox", "children": [ { "$component": "span", "content": "Child 1" }, { "$component": "span", "content": "Child 2" } ] } ``` With an implementation like this: ``` vbox_component = ->(config, env, &render_child) do rendered_children = config['children'].map do |child_component| render_child.call(child_component, env) end "