Skip to content

Components

Linus Hagemann edited this page Aug 23, 2023 · 6 revisions

Best Practices and Known Pitfalls working with Components

Property Reconciliation

Usually, all properties changing on a components or its building blocks when opened in editing mode are reconciled into the code. This does not hold for properties start with an underscore!. Those are never reconciled. The reason for this being that properties starting with an underscore are "internal" properties, by convention.

Setter inside of the ViewModel class vs. onRefresh

Coming from lively.morphic one might be tempted to extensively user setters inside of the properties of a ViewModel. However, when working with components, this has a significant downsise, as soon as the setters logic should interact with the View: There are no guarantees regarding the existence of the View or when it might be created.

The solution to this is to favor the onRefresh method of the ViewModel. It gets called each time a declared property of the ViewModel changes. It gets the name of the changed property as a parameter (as string). It is guaranteed, that this method does not get invoked before the View is present, i.e., after viewDidLoad was called.

Glossary

Let's take a look at the core abstractions when working with components in lively.next. When talking about the component system we have to differentiate between public (user facing) abstractions and internal abstractions.

User Facing

component

Components are the primary form of defining reusable UI elements that can be used to compose new UIs in lively.next. They are declarative definitions of the structure and appearance of morph hierarchies. Components can be defined as standalone or as a derivation from another existing component which enables reusability and avoid style duplications in the system. Components are created by calling the component() function with a nested object (representing the structure and style of the morph hierarchy also known as a spec) or a reference to another component followed by the spec object as the second parameter (now acting as a spec that defines which properties are to be overridden from the parent component). Finally we can also define the custom behaviour corresponding to a particular component in the form of view model classes. Once a morph is instantiate from a component, these view model classes are used to instantiate a view model which implements the custom behaviour of a morph in a separate object. Internally, components are reified as style policies that are wrapped by a Component Descriptor. Among other things this allows the system to keep track of derivations and dependencies between component definitions and morphs and keeping them in sync.

part

A part is an instance of a component, which can be created by calling part method with the corresponding component and overridden properties. Unlike a component it is an actual tangible morph instance with properties that are dictated by the component definition (structure and style). Just as the component() function part() takes a spec of overridden properties as a second optional parameter. The overridden properties work in the same way as they do when deriving a component from another one, except in this case they are isolated to the morph instance we create. This is especially useful for creating last minute custom adjustments to a morph that can not be reasonably served by a dedicated component. The part method can also be used within a component definition. In this context, it denotes that a part of the component being defined is actually constructed based on another component in the system. This opens up another possibility of reusing components in the system, by actually embedding derivations of them within other components.

master

Is the property on a morph that points to the style policy (= component definition, although in this case only the styling aspect of the definition is important, as the structure of the morph which property is set is not touched) that is currently applied to it and its submorphs. This can also be overridden at runtime, however this rarely leads to the desired outcome as it radically resets the entire style of a morph and its submorphs in order to adhere to the new component definition (ignoring any of the overridden properties as part of the part() call).

View Model (viewModel)

Is an object that encapsulates state and behavior of the respective morph hierarchy it is attached to. View models are able to define bindings that listen for events in the morph hierarchy and in turn trigger handlers defined on the view model. They are also able to expose functions and properties through the root morph of the hierarchy they are bound to. This enables a uniform morphic interface from the outside while enabling the encapsulation of behavior into separate objects from the UI.

Internal

Component Scopes

Denotes the fractions of a submorph hierarchy that are managed by a component. When a component definition does not include any reused components (via an embedded part() call) the component scope comprises the entire submorph hierarchy. As soon as derived components are introduced into a component definition, the component scope stops at the point where the other derived component is introduced. Conceptually this a partitioning of a tree into different sub-trees, where the sub-trees are defined by the introduction of component derivations. If this sounds too complicated, dont worry. In practice you will not have to deal with component scopes directly.

Style Policy (policy, policy applicator)

Holds information (the concrete data structure for this is also referred to as a spec) about how a component or single morph should be styled. A style policy also knows about how to apply the style to a particular morph and its submorphs. Finally we are also able to create morphs from style policies by using them as a factory that produces morphs stylized in accordance to the policy.

Inline Policy

These are the product of overwriting a property when instantiating a part. They are referred to as inline since there is no top level component definition they correspond with. Instead they are more or less spontenous customized derivations of components that are only used in a particular part of a component definition. In principle, all inline policies could be replaced by top level/normal style policies however this rarely makes sense since not every style is a meaningful reusable entity.

Inline Master

Refers to the overriding (adjusting) of the master property of a morph as part of a derived component or within an inline policy. Precendence wise, overridden masters are sandwiched between the parent component props and the overridden props when it comes to the synthesization of style properties.

Property Synthesization

Refers to the process of computing the set of style properties for a particular morph inside a morph hierarchy that is being styled by a policy. When synthesization happens the entire derivation chain of a style policy is traversed, collecting the applicable style properties on the go and finally returning a set of properties that are to be applied to the morph in question. The properties are collected per policy and can be divided into three classes with the increasing precedence:

  1. The synthesized properties of the parent policy (if applicable, if this is a top level policy this is dropped).
  2. The synthesized properties of the inline master (if applicable, if there is not inline master for this morph, this is dropped).
  3. The set of locally defined overridden properties for this policy.

Note how in step 1. we recursively invoke the 3 steps outlined above for the parent policy. This is how the traversal of the derivation chain is implemented.

Component Descriptor

Is a data structure that keeps track of dependent components of a component, meta information about the location of the source code, module and the current style policy object. It is mainly intended to be used in tools that aid the user in crafting new components.

Reconciliation

One of the core features of lively.next is to evolve and develop an application via direct manipulation. The system comes with a custom implementation of the Halo interface first introduced in the Self Programming Language.

Unlike other implementations of the Halo system (in Smalltalk, LivelyKernel and others) lively.next tries to reconcile the best practices found in most of todays popular design tools with the original design of the Halo. For instance we provide a classic side bar for quick access to various style properties and structure of the morphs that are worked with. Furthermore the user has access to a classic scene graph, where elements can be quickly obtained and replaced if needed. Also there are two "modes" which allow for either an easy invocation of the halo by left clicking (Halo mode), or direct interaction with the elements in order to experiment with their specific behavior (Interaction mode).

One major issue with many of the past implementations of Morphic is, that the halo interface is only suitable for exploration and, at best, debugging of running applications. Utilizing the halo as a means to evolve and build new applications and interfaces is difficult since there is no way to backfeed these interaction into the applications definition. Therefore it offers no avenue to later on share your results in a way that allows for:

  1. collaboration among multiple developers working on the same project
  2. reusability of said UI artefacts in other contexts and
  3. a compressed overview of the state of the application that can serve as a single source of truth (which in other systems is often achieved by source code).

Past versions of LivelyKernel have explored different possible solutions for this problem. Some approaches favored a more elaborate tool support, which allowed the developers to inspect and manage different versions of snapshots of their morphs, reconcile conflicts, or revert to earlier version of a morph HyperLively. However, this came with the cost of abandoning existing tools the outside world used for managing application development which mostly relied on plain text. Other solutions set out to abandon the custom morph objects approach alltogether and instead resort to HTML and WebComponents as a way to achieve the desired properties outlined above (see Lively4). However this comes at the price of losing a lot of the conveniences a custom morph implementation offers, which is: not having to deal with CSS, not having to worry about serializable state of applications and not having access to a custom event system that avoids some of the pitfalls of the native DOM event system (which is broken in our opinion).

In lively.next we attempt to find another sweet spot between keeping the benefits of morphic as much as possible while also enabling the interaction with existing software collaboration tools available on the internet. The idea here is to translate the artefacts that are built by halo interactions into modular abstractions that can be stored in javascript modules. So the challenge here is to somehow translate the direct manipulation into source code transformations (edit, cut, copy, insert...). This is what we refer to internally as reconciliation.

Reconciliation in the System Browser

When a component module (ending in .cp.js) is opened, the SystemBrowser will highlight the different component definitions inside the module by placing a edit component button next to each one of them. Once clicked, the user gets access to a morph representation of the component definition and is able to manipulate that with the halo and all corresponding tools available. All actions are fed back into the source code in order to keep the component definition consistent with the visual object being manipulated. While these editing sessions are in progress, the text input into the open module is blocked. Should the user try to enter new code into the module, with one or more active editing sessions in progress, the browser prompts them to close all open sessions in order to start entering code. The system browser enforces a strict division between these two modes of code writing (that is code written via reconciliation and code written "by hand").

(Future Work) Reconciliation in the Component Editor

Components can not only be visually opened via the aforementioned "edit component" button, but also be accessed via the components browser. As long as the component is

  1. A core component or
  2. A component that is part of the currently opened project we are able to directly access the visual incarnation of the component just as we did in the example above.

Once we select this visual component with the halo, we can in turn open up the Component Editor. This is a tool that in principle works similar to the Object Editor, except that instead of the class it shows the component definition and instead of the superclasses it show the parent components. The main benefit of the ComponentEditor is however, that it supports a multi modal component development where the user is able to constantly interleave source code adjustments by keyboard input and direct manipulations via reconciliation. This is enabled by a different evaluation mode of the source code editor, that is more akin to a "live editor". This means that the component definition is evaluated and stored away any time we reach a syntactically sound form (when typing). During reconciliation any change is immediately reevaluated and stored since we can only transition between syntactically sound stages (by definition). Should we start to reconcile with unfinished code that is not yet available for storing, it is discarded in order to allow the reconciliation to kick in. Since we genorously store away as soon as the code reaches a parsable state again, the potential of "accidental" loss of code is rather small or only occurs in very pathological scenarios that are unlikely to occur in practice.