Concept Language

Concept Language #

The concept language in Varv is used to define the interactive behavior of applications. The concept language is used in concept definitions: files or pieces of code that use the concept language. Our most common syntax for the concept language is JSON, but also YAML or other authoring tools like block-based visual programming can be used to author code.

Concepts #

The core building blocks of the concept language are—as the name implies—concepts. A concept is an individual named unit of interactive behavior, for instance, a “todo” in a todo list application. In a concept definition, concepts reside inside the "concepts" object. An example for a todo list could look like this:

{
    "concepts": {
        "todo": {
            // A single item
        },
        "todoList": {
            // A list of todos
        },
        "todoInput": {
            // An input to create new todos
        }
    }
}

Concept Instances #

A concept by itself only defines how an instance of a concept should behave once it exists. This can be thought of as similar to classes and objects in other programming languages like Java. Instances of concepts can, e.g., be created using the inspector in Cauldron (see Varv with Webstrates) or by using the "new" action.

Schema #

Each concept can contain a schema, which defines the shape of the state of a concept. This is done using named properties—the schema is a list of properties. The types of properties available in Varv are:

Property Type Description
"boolean" A simple Boolean type.
"string" A simple string type.
"number" A type for integer and float numbers.
"array" An array of any of the other types.
"anotherConceptName" A type that refers to another concept (see example below).

Returning to the todo list example, a schema of such an app could look like the following. Each property needs to be placed inside a "schema" object inside concept objects:

{
    "concepts": {
        "todo": {
            "schema": {
                "label": "string"
            }
        },
        "todoList": {
            "schema": {
                "todos": { "array": "todo" },
                "todosCount": "number"
            }
        },
        "todoInput": {
            "schema": {
                "text": "string"
            }
        }
    }
}

Property Type Parameters #

A property can be defined using the direct notation of the property type itself. For example:

{
    "schema": {
        "myProperty": "string"
    }
}

A property type can, however, also have parameters. For instance, a "string" property can be limited to specific values using an "enum" or the default value of a property can be set using the default "default" parameter. To set these parameters, the property type needs to be expanded to a full object:

{
    "schema": {
        "myFruit": { "string": {
            "enum": [ "Apple", "Orange", "Banana" ],
            "default": "Apple"
        }}
    }
}
Full Parameter List: A full list of property type parameters can be found in the Schema part of the user guide.

Shorthand Notations #

Some properties (and later also actions) like the "array" require at least one parameter to be used. To make writing them easier shorthand notations can be used to define these primary parameters. In the "array", for instance, this primary parameter is "items", which defines the type of items in this array. Normally one would write:

{
    "schema": {
        "myStringArray": { "array": { "items": "string" }}
    }
}

The shorthand for this is to omit the object and directly use the item type:

{
    "schema": {
        "myStringArray": { "array": "string" }
    }
}
This is also what we do above in the todo list example to set the property "todos": { "array": "todo" }.

Using the shorthand notation only allows to set the primary parameter. To also set other additional parameters like "default" requires to expand the notation in the full object.

Derived Properties #

Sometimes a property depends on other properties and its value is derived from them. For instance, in the todo list example from above the property "todosCount" should always show the count of items in the "todos" property.

When deriving properties in Varv, we need to define from which "properties" they are derived and how the values should "transform" inside the "derive" parameter of a property. "properties" takes an array of property names and "transform" takes an action chain (actions and action chains are explained in the next section):

{
    "schema": {
        "todos ": { "array": "todo" },
        "todosCount": { "number": {
            "derive": {
                "properties": [ "todos" ],
                "transform": [
                    { "length": "todos" }
                ]
            }
        }}
    }
}

In this example, the property "todosCount" is derived only from the property "todos" and should be transformed by the action "length" which takes an array as input and returns a number. Varv handles updating the derived value automatically.

In addition to using properties from the same concept in the "properties" list, it is also possible to add properties from other concepts using the dot-notation, e.g., "myOtherConcept.myProperty". The "concepts" option, further, allows to derive a property when concept instances are added or removed:

{
    "user": { "schema": {
        "name": "string",
        "role": "string"
    }},
    "userManager": { "schema": {
        "adminExists": { "boolean": {
            "derive": {
                "concepts": [ "user" ],
                "properties": [ "user.role" ],
                "transform": [
                    { "exists": {
                        "concept": "user",
                        "where": {
                            "property": "role",
                            "equals": "admin"
                        }
                    }}
                ]
            }
        }}
    }}
}

In the above example, the property "adminExists" is derived whenever a user concept instance is added or removed, and whenever the "role" property of any user is changing.

The "properties" parameter can also be omitted. Doing so, however, causes the property to not be live updated when used in views. Whenever a derived property with the "properties" parameter is used in actions, it will update its value.

Actions and Triggers #

Actions in Varv provide an abstraction for specifying state transformations, i.e., the properties of concepts. Actions consist of an optional when-block and a required then-block. Actions are named and placed inside the "actions" object inside concepts.

{
    "concepts": {
        "todo": {
            "schema": {},
            "actions": {}
        },
        "todoList": {
            "schema": {},
            "actions": {}
        },
        "todoInput": {
            "schema": {},
            "actions": {}
        }
    }
}

When-Block and Triggers #

The when-block defines an array of triggers that cause the action to be executed. Triggers can either be reactive triggers or view triggers. A reactive trigger reacts to, e.g., state changes in properties of concepts or are triggered at a given time interval—or react to when other actions are run. View triggers, on the other hand, are triggered when input events happen in the view, for instance, mouse clicks or key presses in the DOM View.

The when-block is located in the object of an action as the "when" object. By default it takes an array of triggers, if only one trigger is used a shorthand notation omitting the array is possible. The following two when-blocks behave the same:

{
    "actions": {
        "myAction": {
            "when": [
                { "key": { "key": "Enter" }},
                { "interval": 1000 }
            ]
        },
        "myShorthandAction": {
            "when": { "click": "todo" }
        }
    }
}

Similar to properties, triggers (and also actions) can have parameters and shorthand notations to write them. For instance, in the example { "interval": 1000 } is the shorthand for { "interval": { "interval": 1000 }} (the first interval refers to the trigger, the second to the parameter).

Then-Block and Action Chains #

The then-block specifies an array of actions—an action chain—that should be executed. Varv provides a series of primitive actions that can be used inside actions chains.

For example, primitive actions can be "increment" to increment numbers, "length" to get the length of an array, or "apppend" to add items to an array. Other primitive actions can also determine the control flow (e.g., forking the chain to execute an independent action) Additionally, any other action that was defined in a Varv application can be used within an action chain—even the same one as it is used in (this allows, e.g., for recursion).

Actions and Trigger List: A list of actions and triggers can be found in the Actions and Triggers part of the user guide, as well as in the Reference.

The then-block is located inside a named action as the "then" object. Like the when-block, it can take either a single action or an array of actions. If we in the todo list example from earlier, for example, want to delete a todo when a user clicks on it we can write an action like this ("remove" is an action to delete concept instances):

{
    "concepts": {
        "todo": {
            "actions": {
                "deleteOnClick": {
                    "when": { "click": "todo" },
                    "then": "remove"
                }
            }
        }
    }
}

Actions without When-Block #

If an action has only a then-block, for instance, if it is used nested within other actions, the when-block can be omitted and the action chain placed directly as an array into the named action object:

{
    "actions": {
        "incrementCountByFive": [
            { "increment": { "property": "count", "by": 5 }}
        ]
    }
}

Variables #

The output of events are stored as variables and can be referenced using the dollar sign. By default, the variable has the name of the action, so the output of the "length" action could be referenced using $length. Using the "as" parameter, the output can also be stored in a variable with a custom name.

For instance, in the "todoInput" concept, we can store the current text from the input field using the "get" action, then empty the input field using the "set" action, and, lastly, call the "addNewTodo" action (see next section) to create a new todo:

{
    "concepts": {
        "todoInput": {
            "schema": {
                "text": "string"
            },
            "actions": {
                "sendInput": [
                    { "get": {
                        "property": "todoInput.text",
                        "as": "inputText"
                    }},
                    { "set": {
                        "property": "todoInput.text",
                        "value": ""
                    }},
                    { "addNewTodo": { "newTodoLabel": "$inputText" }}
                ]
            }
        }
    }
}
Event Flow: The structure of events and how they are passed through action chains is explained in the guide section Event Flow.
Dot Notation: The dot notation used, e.g., in "todoInput.text" is explained below.

Parameters #

Actions in concepts can also define their own parameters, which can then be used when using the action in another action chain. This is done using the @-symbol in front of a string value, e.g., @newTodoLabel. In the following example we define the "addNewTodo" action with the parameter newTodoLabel that was used in the previous section:

{
    "concepts": {
        "todoList": {
            "schema": {
                "todos": { "array": "todo" }
            },
            "actions": {
                "addNewTodo": [
                    { "new": {
                        "concept": "todo",
                        "with": { "label": "@newTodoLabel" },
                        "as": "newTodo"
                    }},
                    { "append": {
                        "property": "todoList.todos",
                        "item": "$newTodo"
                    }}
                ]
            }
        }
    }
}

Action Lookup Order #

Understanding Targets: To understand some aspects of the action lookup order please first check the Event Flow section to understand what a targets in an event are.

Action names in concepts are unique within a single concept. Across multiple concepts, however, the same action name can be used. For example, one could imagine both a todo item and the whole todo list to have the action "isDone": one checking if a single todo is done, the other checking if all todos in the list are done.

When calling the "isDone" action within an action chain, this is ambiguous. Varv, therefore, has a lookup order for actions in order to guess which action should be used:

  1. Dot Notation: To force the execution of an action of a specific concept it can be prepended the action name. For example, "todo.isDone" or "todoList.isDone".
  2. Primitive Action: If the dot notation is not used, Varv checks whether there is an primitive action with the given name.
  3. Target: If no primitive action was found, Varv will check the actions in the concept type of the current target of the event. For example, if the current target is of the type "todo" it will use the "isDone" action from it.
  4. First Match: If the action is not found within the concept of the target, Varv will look through all concepts in the current model and execute the first action with a matching name. The “first” here means the first that was defined inside a concept definition.

Dot Notation for Properties #

When selecting properties in actions, e.g., in the action { "get": "myProperty" }, the action tries to get the property from the current target of the event. To refer to properties of a particular concept, which might also be different than the target, a dot notation can be used to refer to them. Consider the following example:

{
    "concepts": {
        "todoInput": {
            "schema": { "text": "string" },
            "actions": {
                "activateInput": [
                    { "get": { "property": "text", "as": "text" }},
                    { "set": { "text": "" }},
                    { "addNewTodo": { "newTodoText": "$text" }}
                ]
            }
        },
        "todoList": {
            "schema": { "todos": { "array": "todo" }},
            "actions": {
                "addNewTodo": [
                    { "new": { "concept": "todo", "with": { "text": "@newTodoText" }}},
                    { "append": { "property": "todoList.todos", "item": "$new" }}
                ]
            }
        }
    }
}

In the action "addNewTodo" the "append" action appends the new element to the property todoList.todos. This is required because the current target of the event at the "append" action is the new todo that was created by the "new" action (the "new" action changes the target to the newly created instance). The "append" action would therefore attempt to add the new todo to the todos property of the new instance of the concept todo, which does not have a todos property and run into an error. The dot notation specifies that the todos property of the todoList concept should be used.

Works also in Shorthands: The dot notation also works in shorthand notations like in the "set" action: { "set": { "todoInput.text": "Hello World" }}.
Use only for concept with a single instance: When using the dot notation for properties, e.g., todoList.todos, Varv has no information about which instance of a concept should be used, i.e., the property todos from which todoList instance. In such a case, Varv selects the first concept instance of the given concept type—this might lead to unexpected behavior. Therefore, we recommend using this notation only for concept that have a single instance.

Using Variables and Properties in Strings #

When using actions that take parameters with type string, a $-notation can be used to insert values into a string. For example:

{
    "myConcept": {
        "schema": { "myProperty": "string" },
        "actions": {
            "testAction": [
                { "set": { "myConcept.myProperty": "Property" }},
                { "set": { "variable": "myVariable", "value": "Variable" }},
                { "debugMessage": "Hello, myProperty is $myConcept.myProperty$ and myVariable is $myVariable$." }
            ]
        }
    }
}

This will log in the console Hello, myProperty is Property and myVariable is Variable..

Using Calculations in number Parameters #

In parameters of the type number it is possible to use a calculation (see also "calculate" action). For example, imagine using "slice" which takes "start" and "end" parameters but one only has the start index and the count of items, so one can use a calculation for the "end" index:

{
    "slice": {
        "of": { "property": "myArrayProperty" },
        "start": "$startIndex",
        "end": { "calculate": "$startIndex$ + $count$" }
    }
}

Concept Definition Merging #

In Varv, it is possible to create any number of concept definitions and use them in conjunction. When multiple concept definitions are detected by Varv, it will attempt to merge all definitions Concept, schema, and attributes from later concept definitions will overwrite objects from earlier definitions. Consider the following example consisting of two concept definitions:

{
    "concepts": {
        "todo": {
            "schema": { "label": "string" },
            "actions": {
                "deleteTodoOnClick": {
                    "when": { "click": "todo" },
                    "then": { "remove" }
                }
            }
        }
    }
}
{
    "concepts": {
        "todo": {
            "schema": { "done": "boolean" },
            "actions": {
                "deleteTodoOnClick": {
                    "when": { "contextmenu": "todo" }
                },
                "toggleTodo": {
                    "when": { "click": "todo" },
                    "then": { "toggle": "done" }
                }
            }
        }
    }
}

The first definition create the todo concept with a label property and an action to delete a todo when clicking it. The second definition adds a done property, overwrites the when-block of the delete action to work now with a right-click (the "contextmenu" trigger), and adds a new action to toggle todos done. The merged concept definition would look like this:

{
    "concepts": {
        "todo": {
            "schema": {
                "label": "string",
                "done": "boolean"
            },
            "actions": {
                "deleteTodoOnClick": {
                    "when": { "contextmenu": "todo" },
                    "then": { "remove" }
                },
                "toggleTodo": {
                    "when": { "click": "todo" },
                    "then": { "toggle": "done" }
                }
            }
        }
    }
}

This makes it possible to add, modify, or suppress existing interactive behavior without modifying the original code of an application.

Extensions #

Extensions are a mechanism that enables reusing concepts or parts of them. While Varv supports the merging of concept definition files to extend functionality, this is sometimes not flexible enough, for instance, when some properties or actions should be used by multiple concepts. It is, further, not possible to combine multiple concepts easily.

To support a more flexible mechanism for reusing concepts, Varv offers four extension operators: "inject", "join", "omit", and "pick". In brief, they support the following:

Extension Description
"inject" This operator takes one or multiple source concepts and one target concept, and injects the source concepts into the target concept, leaving the source concepts unaltered.
"join" This operator takes two or more source concepts and merges their schema and actions, creating a new target concept.
"omit" This operator takes an source concept and removes schema or actions from it.
"pick" This operator takes an source concept and selects a subset of its schema and actions to create a new target concept, leaving the source concept unaltered.

These extension operators, for instance, allow to create mixins that can be reused in multiple other contexts.

Extensions List: A detailed list of each of the extension operators and their use is described in the Extensions section.

Location in Concept Definitions #

Extension operators are placed inside an "extensions" object that can either reside in the root object or inside a named concept. Inside the "extensions" takes an array of extensions:

{
    "concepts": {
        "todo": {
            "schema": { "label": "string" },
            "extensions": {
                "inject": [ "completable" ]
            }
        },
        "completable": {
            "schema": { "done": "boolean" },
            "actions": {
                "toggleDone": {
                    "when": { "click": "completable" },
                    "then": { "toggle": "done" }
                }
            }
        }
    },
    "extensions": [
        { "concept": "todo", "inject": "completable" }
    ]
}

In this example, the "completable" concept is being injected into the "todo" concept (we added the extension both in the concept and in the root object to illustrate where they can be placed, they should be only placed in one of the two locations). Both the property "done" and the action "toggleDone" are injected, adding them to the "todo" concept. Any future changes to the "completable" concept are also automatically injected to the "todo" concept.

Order is Important: The order in which extensions are defined in the concept definitions is important as they are applied in that order and possibly overwrite parts of schema or actions in different ways. Please read the next section for further information.

Extension and Merging Order #

Extensions and the merging of concept definitions can both be used to modify existing concepts. As a general order, first, all concept definitions are merged into one big concept definition. In the second step, all extensions are applied in the order they appear in the document. This order can be important, as some operators, for instance "inject", overwrite schema and actions of the target concept. If multiple concepts are injected that overwrite a certain property or action, all earlier extensions will be overwritten again.

Simple Polymorphism Support #

When injecting concepts into another or joining concepts, Varv stores information about which concepts got injected/which concept were joined. This allows for simple polymorphism in Varv. For example, consider the following case:

{
    "concepts": {
        "shape": {
            "schema": {
                "x": "number",
                "y": "number",
                "width": "number",
                "height": "number"
            }
        },
        "rectangle": {},
        "circle": {}
    },
    "extensions": [
        { "concept": "rectangle", "inject": "shape" },
        { "concept": "circle", "inject": "shape" }
    ]
}

And the following template:

<dom-view-template>
    <div concept="shape"> ... </div>
</dom-view-template>

The template would render both concept instances from the concept shape but also rectangle and circle.

Full Simple Todo List Example #

A full example of a simple todo list that includes many of the above examples can be found here: Simple Todo List.


© 2023 Aarhus University | Made by cavi.au.dk | Contact Person: Clemens Nylandsted Klokmose