velix_editor 0.1.2 copy "velix_editor: ^0.1.2" to clipboard
velix_editor: ^0.1.2 copied to clipboard

Velix Wysiwyg UI editor and runtime engine.

Velix UI Editor #

This package implements a wysiwyg UI editor and runtime engine with JSON as the persistence format.

Goals and Design Principles #

Developer tool #

The editor and the engine are ment to simplify the development process but not replace it. So, it's not one of those no-code tools ( e.g. FlutterFlow ) but more a developer tool.

On the other hand it still offers all of the typical features of a wysiwyg editor:

  • drag & drop
  • widget tree
  • dynamic palette
  • property editors
  • undo
  • live preview
  • i18n
  • shortcuts
image

Currently the approach is to be able to design individual pages only.

Model Based #

Every aspect is model based and pluggable avoiding any hardcoded logic. This relates to different aspects.

  • the set of widget types
  • the configuration data of every widget
  • the component that is responsible to render a widget
  • the property editors that are rendered.

Let's look at the different aspects:

Widget Data Definition #

A widget is defined by a set of configuration properties declared as a class.

Example

@Dataclass()
@DeclareWidget(name: "button", i18n: "editor:widgets.button.title", group: "widgets", icon: Icons.text_fields)
@JsonSerializable(discriminator: "button")
class ButtonWidgetData extends WidgetData {
  // instance data

  @DeclareProperty(groupI18N: "editor:groups.general", i18n: "editor:widgets.button.label")
  String label;
  @DeclareProperty(group: "Font Properties")
  Font? font;
  @DeclareProperty(group: "Layout Properties")
  Padding? padding;

  // constructor

  ButtonWidgetData({super.type = "button", super.children = const [], required this.label, this.font, this.padding});
}

Different annotations are used to register the widget type and its meta-data on startup. This process relies on the basic velix code generator triggered by the @Dataclass annotation.

Widget Renderer #

Every widget type - in our case a button type - requires a renderer, that will consume the configuration properties.

Example


@Injectable()
class ButtonWidgetBuilder extends WidgetBuilder<ButtonWidgetData> {
  // constructor

  ButtonWidgetBuilder() : super(name: "button");

  // internal

  // override

  @override
  Widget create(ButtonWidgetData data, Environment environment) {
    return ElevatedButton(
      ...
    );
  }
}

The @Injectable annotation is used by the velix dependency injection framework and will make sure, that a singleton instance is created on startup of the corresponding container, which will in turn register the builder in a widget registry.

Property Editor #

The same logic is applied for property editors, which are used in a generic property panel.

Example

Injectable()
class StringEditorBuilder extends PropertyEditorBuilder<String> {
  // override

  @override
  Widget buildEditor({
    required MessageBus messageBus,
    required CommandStack commandStack,
    required FieldDescriptor property,
    required String label,
    required dynamic object,
    required dynamic value,
    required ValueChanged<dynamic> onChanged,
  }) {
    return _StringEditorStateful(
      label: label,
      value: value ?? "",
      onChanged: onChanged,
    );
  }
}

This class will register an editor for properties of type String.

JSON data format #

Widgets are stored as a tree-structure in JSON that is a 1:1 mapping of the properties and defined converters ( e.g. font weight ).

Example

{
  "type": "container",
  "children": [
    {
    "type": "text",
    "children": [],
    "label": "Hello World"
    },
    {
    "type": "button",
    "children": [],
    "label": "PRESS",
    "font": {
      "size": 12,
      "weight": 100,
      "style": "normal"
      },
    "padding": {
      "left": 1,
      "top": 1,
      "right": 1,
      "bottom": 1
      }
    },
    {
    "type": "text",
    "children": [],
    "label": "Second Text"
    }
  ]
}

Generic approach avoiding code generation #

The framework tries to avoid any additional - on top of the meta-data - code generators, as they introduce additional build steps and dependencies which can be avoided.

The only problematic part are actions, that need to access the surrounding infrastructure ( e.g. a page method ) including passing parameters - literal or variable references - or simple parameter expressions.

The current approach is to utilize generated meta-data ( e.g. class structures ) in order to allow for smart input code editors, and to evaluate the resulting expressions based on the concrete runtime data using the expression library.

Work in progress :-).