Skip to content

RichText

Available since v2.0.18

RichText is a component for editing and displaying formatted multi-line text. The user formats the text with a toolbar: headings, bold, italic, underline, strikethrough, lists, quotes, code and text color. The value is stored as Markdown, the same format that Yandex Wiki uses.

Basics

Live Sample · GitHub

How does it look?

img_list.png

img_info.png

img_form.png

The formatting is shown in view mode and in edit mode. The toolbar is shown in edit mode only. See Formatting for what the toolbar can do.

Warning

The value is stored with formatting marks, for example **bold**. If a field stores richText, use the richText type on every widget that shows this field. A widget with another type, for example text, shows the marks as they are.

Info

The supported content size is up to 8,000 characters, formatting marks included.

How to add?

Example

Step1 Add String field to corresponding BaseEntity. The value keeps formatting marks, so the column needs more room than for plain text.

@Entity
@Getter
@Setter
@NoArgsConstructor
public class MyEntity extends BaseEntity {

    /** Stores markdown, so it needs far more room than a plain one-line text field. */
    @Column(length = 10000)
    private String customField;

}

Step2 Add field String to corresponding DataResponseDTO.

@Getter
@Setter
@NoArgsConstructor
public class MyExampleDTO extends DataResponseDTO {

    @SearchParameter(name = "customField")
    private String customField;

    public MyExampleDTO(MyEntity entity) {
        this.id = entity.getId().toString();
        this.customField = entity.getCustomField();
    }

}

Step3 Add to .widget.json.

{
  "name": "MyExampleList",
  "title": "List title",
  "type": "List",
  "bc": "myExampleBc",
  "fields": [
    {
      "title": "Custom Field",
      "key": "customField",
      "type": "richText"
    }
  ]
}

Step3 Add to .widget.json.

{
  "name": "MyExampleInfo",
  "title": "Info title",
  "type": "Info",
  "bc": "myExampleBc",
  "fields": [
    {
      "label": "Custom Field",
      "key": "customField",
      "type": "richText"
    }
  ],
  "options": {
    "layout": {
      "rows": [
        {
          "cols": [
            {
              "fieldKey": "customField",
              "span": 24
            }
          ]
        }
      ]
    }
  }
}

Step3 Add to .widget.json.

{
  "name": "MyExampleForm",
  "title": "Form title",
  "type": "Form",
  "bc": "myExampleBc",
  "fields": [
    {
      "label": "Custom Field",
      "key": "customField",
      "type": "richText"
    }
  ],
  "options": {
    "layout": {
      "rows": [
        {
          "cols": [
            {
              "fieldKey": "customField",
              "span": 24
            }
          ]
        }
      ]
    }
  }
}

Live Sample · GitHub

Placeholder

Live Sample · GitHub

Placeholder allows you to provide a concise hint, guiding users on the expected value. This hint is displayed before any user input. It can be calculated based on business logic of application. It is shown for a Readonly field as well.

Visual editor: since release 3.0.2

Up to and including release 3.0.1, the placeholder is shown only in the Markdown markup mode, the visual editor shows an empty field. Since release 3.0.2, the placeholder is shown in both modes.

How does it look?

img_plchldr_list.png

not applicable

img_plchldr_form.png

How to add?

Example

Add fields.setPlaceholder to corresponding FieldMetaBuilder.

    @Override
    public void buildRowDependentMeta(RowDependentFieldsMeta<MyExampleDTO> fields, InnerBcDescription bcDescription,
                                      Long id, Long parentId) {
        fields.setEnabled(MyExampleDTO_.customField);
        fields.setPlaceholder(MyExampleDTO_.customField, "Placeholder text");
        fields.setPlaceholder(MyExampleDTO_.customFieldRO, "Placeholder text");
    }

Works for List.

not applicable

Works for Form.

Live Sample · GitHub

Color

not available

Readonly/Editable

Readonly/Editable indicates whether the field can be edited or not. It can be calculated based on business logic of application

Editable Live Sample · GitHub

Readonly Live Sample · GitHub

How does it look?

img_edit_list.png

not applicable

img_edit_form.png

img_ro_list.png

img_ro_info.png

img_ro_form.png

How to add?

Example

Step1 Add mapping DTO->entity to corresponding VersionAwareResponseService.

    @Override
    protected ActionResultDTO<MyExampleDTO> doUpdateEntity(MyEntity entity, MyExampleDTO data,
                                                             BusinessComponent bc) {
        if (data.isFieldChanged(MyExampleDTO_.customField)) {
            entity.setCustomField(data.getCustomField());
        }

        return new ActionResultDTO<>(entityToDto(bc, entity));
    }
Step2 Add fields.setEnabled to corresponding FieldMetaBuilder.
    @Override
    public void buildRowDependentMeta(RowDependentFieldsMeta<MyExampleDTO> fields, InnerBcDescription bcDescription,
                                      Long id, Long parentId) {
        fields.setEnabled(MyExampleDTO_.customField);
    }

Works for List.

not applicable

Works for Form.

Live Sample · GitHub

Option 1 Enabled by default.

    @Override
    public void buildRowDependentMeta(RowDependentFieldsMeta<MyExampleDTO> fields, InnerBcDescription bcDescription,
                                      Long id, Long parentId) {
        //
    }
Option 2 Not recommended. Property fields.setDisabled() overrides the enabled field if you use after property fields.setEnabled.

Works for List.

Works for Info.

Works for Form.

Live Sample · GitHub

Filtering

Filtering allows you to search data based on criteria. For MyExample field filtering is case-insensitive and retrieves records containing the specified value at any position (similar to SQL Like %value% ).

Plain text

Live Sample · GitHub

The filter compares only letters and digits, so formatting marks do not get in the way: metal type finds "Metal type".

Default not recommended

Live Sample · GitHub

The filter searches the stored value with formatting marks: metal type does not find "Metal type", because the bold marks stand between the words (**Metal** type).

How does it look?

img_filtr_list.gif

not applicable

not applicable

img_filtr_default_list.gif

not applicable

not applicable

How to add?

Example

Step 1 Add a column with only the letters and digits of the value to corresponding BaseEntity. The database computes it, the application does not write it. In the sample the table is created by Hibernate, so the column is described in columnDefinition (PostgreSQL). If the table is created by Liquibase, add the column by the SQL of Step 5.

@Entity
@Getter
@Setter
@NoArgsConstructor
public class MyEntity extends BaseEntity {

    /** Stores markdown, so it needs far more room than a plain one-line text field. */
    @Column(length = 10000)
    private String customField;

    /**
     * Only the letters and digits of customField, the database computes it (PostgreSQL).
     * The filter searches this column, so formatting marks do not get in the way.
     */
    @Column(insertable = false, updatable = false,
            columnDefinition = "text GENERATED ALWAYS AS (regexp_replace(custom_field, '[^[:alnum:]]', '', 'g')) STORED")
    private String customFieldPlain;

}
Step 2 Add a provider that keeps only the letters and digits of the value the user typed in the filter.
/**
 * Keeps only the letters and digits of the value the user typed in the filter,
 * the same way the column customFieldPlain keeps them.
 */
@Component
@EqualsAndHashCode(callSuper = false)
public class PlainTextValueProvider extends AbstractClassifyDataProvider implements ClassifyDataProvider {

    @Override
    protected List<ClassifyDataParameter> getProviderParameterValues(Field dtoField, ClassifyDataParameter dataParameter,
            FilterParameter filterParam, SearchParameter searchParam, List<ClassifyDataProvider> providers) {
        dataParameter.setValue(filterParam.getStringValue().replaceAll("[^\\p{L}\\p{N}]", ""));
        return List.of(dataParameter);
    }

}
Step 3 Add @SearchParameter with this column and this provider to the field of corresponding DataResponseDTO. (Advanced customization SearchParameter)
@Getter
@Setter
@NoArgsConstructor
public class MyExampleDTO extends DataResponseDTO {

    /** The filter of this field compares only letters and digits: of the column and of the value the user typed. */
    @SearchParameter(name = "customFieldPlain", provider = PlainTextValueProvider.class)
    private String customField;

    public MyExampleDTO(MyEntity entity) {
        this.id = entity.getId().toString();
        this.customField = entity.getCustomField();
    }

}
Step 4 Add fields.enableFilter to corresponding FieldMetaBuilder.
    @Override
    public void buildIndependentMeta(FieldsMeta<MyExampleDTO> fields, InnerBcDescription bcDescription, Long parentId) {
        if (configuration.getForceActiveEnabled()) {
            fields.setForceActive(MyExampleDTO_.customField);
        }
        fields.enableFilter(MyExampleDTO_.customField);
    }
Step 5 For a large table, add an index. The filter sends upper(column) LIKE '%VALUE%', so the index is built on upper(column).

-- the column, if the table is created by Liquibase
ALTER TABLE my_entity ADD COLUMN custom_field_plain text
  GENERATED ALWAYS AS (regexp_replace(custom_field, '[^[:alnum:]]', '', 'g')) STORED;
-- the index for LIKE '%value%'
CREATE EXTENSION IF NOT EXISTS pg_trgm;
CREATE INDEX my_entity_custom_field_plain_trgm ON my_entity USING gin (upper(custom_field_plain) gin_trgm_ops);

-- the column, for a VARCHAR2 column CUSTOM_FIELD
ALTER TABLE MY_ENTITY ADD CUSTOM_FIELD_PLAIN VARCHAR2(4000)
  GENERATED ALWAYS AS (REGEXP_REPLACE(CUSTOM_FIELD, '[^[:alnum:]]', '')) VIRTUAL;
An ordinary Oracle index does not speed up LIKE '%value%': the value starts with %.

Restriction

The filter compares only letters and digits, in the column and in the typed value: spaces, punctuation and formatting marks are ignored. So metal type, metaltype and Metal-Type find "Metal type" alike. The name of a text color and the address of a link are letters too, so they are found as well.

Works for List.

not applicable

not applicable

Live Sample · GitHub

Not recommended. The filter searches the stored value with formatting marks.

Step 1 Add @SearchParameter to corresponding DataResponseDTO. (Advanced customization SearchParameter)

@Getter
@Setter
@NoArgsConstructor
public class MyExampleDTO extends DataResponseDTO {

    @SearchParameter(name = "customField")
    private String customField;

    public MyExampleDTO(MyEntity entity) {
        this.id = entity.getId().toString();
        this.customField = entity.getCustomField();
    }

}
Step 2 Add fields.enableFilter to corresponding FieldMetaBuilder.
    @Override
    public void buildIndependentMeta(FieldsMeta<MyExampleDTO> fields, InnerBcDescription bcDescription, Long parentId) {
        if (configuration.getForceActiveEnabled()) {
            fields.setForceActive(MyExampleDTO_.customField);
        }
        fields.enableFilter(MyExampleDTO_.customField);
    }

Works for List.

not applicable

not applicable

Live Sample · GitHub

Drilldown

not available

Validation

Validation allows you to check any business rules for user-entered value. There are types of validation:

1) Exception:Displays a message to notify users about technical or business errors.

Business Exception: Live Sample · GitHub

Runtime Exception: Live Sample · GitHub

2) Confirm: Presents a dialog with an optional message, requiring user confirmation or cancellation before proceeding.

Live Sample · GitHub

3) Field level validation: shows error next to all fields, that validation failed for

Option 1: Live Sample · GitHub

Option 2: Live Sample · GitHub

The value is checked as it is stored, with formatting marks. In the samples the value can contain no more than 50 characters: "A short note." counts 17 characters (A **short** note.), not 13.

How does it look?

img_business_error

img_runtime_error

img_confirm_form

img_javax_stat_form

not applicable

img_business_error

img_runtime_error

img_confirm_form

img_javax_stat_form

How to add?

Example

BusinessException describes an error within a business process.

Add BusinessException to corresponding VersionAwareResponseService.

    @Override
    protected ActionResultDTO<MyExampleDTO> doUpdateEntity(MyEntity entity, MyExampleDTO data,
                                                             BusinessComponent bc) {
        if (data.isFieldChanged(MyExampleDTO_.customField)) {
            entity.setCustomField(data.getCustomField());
            if (StringUtils.length(data.getCustomField()) > 50) {
                throw new BusinessException().addPopup(MAX_50_CHARACTERS);
            }
        }

        return new ActionResultDTO<>(entityToDto(bc, entity));
    }

Works for List.

not applicable

Works for Form.

Live Sample · GitHub

RuntimeException describes technical error within a business process.

Add RuntimeException to corresponding VersionAwareResponseService.

    @Override
    protected ActionResultDTO<MyExampleDTO> doUpdateEntity(MyEntity entity, MyExampleDTO data,
                                                             BusinessComponent bc) {
        if (data.isFieldChanged(MyExampleDTO_.customField)) {
            entity.setCustomField(data.getCustomField());
            try {
                //call custom function
                throw new Exception("Error");
            } catch (Exception e) {
                throw new RuntimeException("An unexpected error has occurred.");
            }
        }

        return new ActionResultDTO<>(entityToDto(bc, entity));
    }

Works for List.

not applicable

Works for Form.

Live Sample · GitHub

Add PreAction.confirm to corresponding VersionAwareResponseService.

    @Override
    public Actions<MyExampleDTO> getActions() {
        return Actions.<MyExampleDTO>builder()
                .action(act -> act
                        .action("save", "save")
                        .withPreAction(PreAction.confirm(cf -> cf
                                .text("You want to save the value?")
                        )))
                .build();
    }

Works for List.

not applicable

Works for Form.

Live Sample · GitHub

Use if:

Requires a simple fields check (javax validation)

Add javax.validation to corresponding DataResponseDTO.

@Getter
@Setter
@NoArgsConstructor
public class MyExampleDTO extends DataResponseDTO {

    @SearchParameter(name = "customField")
    @Size(max = 50, message = MAX_50_CHARACTERS)
    private String customField;

    public MyExampleDTO(MyEntity entity) {
        this.id = entity.getId().toString();
        this.customField = entity.getCustomField();
    }

}

Works for List.

not applicable

Works for Form.

Live Sample · GitHub

Create custom service for business logic check.

Use if:

Business logic check required for fields

Step 1 Create custom method for check.

    private void validateFields(BusinessComponent bc, MyExampleDTO dto) {
        BusinessError.Entity entity = new BusinessError.Entity(bc);
        if (StringUtils.length(dto.getCustomField()) > 50) {
            entity.addField(MyExampleDTO_.customField.getName(), "The field 'customField' can contain no more than 50 characters.");
        }
        if (StringUtils.length(dto.getCustomFieldAdditional()) > 50) {
            entity.addField(
                    MyExampleDTO_.customFieldAdditional.getName(),
                    "The field 'customFieldAdditional' can contain no more than 50 characters."
            );
        }
        if (!entity.getFields().isEmpty()) {
            throw new BusinessException().setEntity(entity);
        }
    }
Step 2 Add custom method for check to corresponding VersionAwareResponseService.
    @Override
    protected ActionResultDTO<MyExampleDTO> doUpdateEntity(MyEntity entity, MyExampleDTO data,
                                                              BusinessComponent bc) {
        validateFields(bc, data);
        if (data.isFieldChanged(MyExampleDTO_.customFieldAdditional)) {
            entity.setCustomFieldAdditional(data.getCustomFieldAdditional());
        }
        if (data.isFieldChanged(MyExampleDTO_.customField)) {
            entity.setCustomField(data.getCustomField());
        }

        return new ActionResultDTO<>(entityToDto(bc, entity));
    }

Live Sample · GitHub

Sorting

Live Sample · GitHub

Sorting allows you to sort data in ascending or descending order. MyExample field is a text field, so lexicographic sorting by the stored value is used for it.

How does it look?

img_sort_list

not applicable

not applicable

How to add?

Example

see more Sorting

Step 1 Add fields.enableSort to corresponding FieldMetaBuilder.

    @Override
    public void buildIndependentMeta(FieldsMeta<MyExampleDTO> fields, InnerBcDescription bcDescription, Long parentId) {
        if (configuration.getForceActiveEnabled()) {
            fields.setForceActive(MyExampleDTO_.customField);
        }
        fields.enableFilter(MyExampleDTO_.customField);
        fields.enableSort(MyExampleDTO_.customField);
    }
Live Sample · GitHub

not applicable

not applicable

Required

Live Sample · GitHub

Required allows you to denote, that this field must have a value provided. By default, RichText field is not required

How does it look?

img_req_list.png

not applicable

img_req_form.png

How to add?

Example

Add fields.setRequired to corresponding FieldMetaBuilder.

    @Override
    public void buildRowDependentMeta(RowDependentFieldsMeta<MyExampleDTO> fields, InnerBcDescription bcDescription,
                                      Long id, Long parentId) {
        fields.setEnabled(MyExampleDTO_.customField);
        fields.setRequired(MyExampleDTO_.customField);
    }

Works for List.

not applicable

Works for Form.

Live Sample · GitHub

Additional properties

Editing modes

Live Sample · GitHub

The settings button (gear) on the toolbar switches the editor:

  • Visual Editor - the formatted text (the default mode);
  • Markdown markup - shows and edits the value as it is stored, with formatting marks.

A read-only or disabled field always shows the formatted text.

How does it look?

img_visual_mode.png

img_markdown_mode.png

How to add?

Example

Works by default for the richText field, nothing to add.

Field height

Live Sample · GitHub

The height of the field is set in rows. A row is as high as a line of the text field, a heading takes more room.

Parameter Mode Default
minRows view mode (List, Info) 1
maxRows view mode (List, Info) 10
editMinRows edit mode (Form, inline editing in List); if not set, minRows is used 5
editMaxRows edit mode (Form, inline editing in List); if not set, maxRows is used 10
  • In view mode, a value longer than maxRows is cut: the last row fades out and ends with .... Hovering over ... shows the full value in a tooltip.
  • In edit mode, the field grows with the text from editMinRows to editMaxRows, then a scroll bar appears. The user can drag the bottom right corner of the field within these limits.

Tip

To show the whole value on Info, set a maxRows that the value never reaches.

How does it look?

img_height_list_default.png img_height_list.png

img_height_info_default.png img_height_info.png

img_height_form_default.png img_height_form.png

How to add?

Example

Add "maxRows", "editMinRows", "editMaxRows" to the field in .widget.json.

{
  "name": "MyExampleList",
  "title": "maxRows 2, editMinRows 3, editMaxRows 6",
  "type": "List",
  "bc": "myExampleBc",
  "fields": [
    {
      "title": "Custom Field",
      "key": "customField",
      "type": "richText",
      "maxRows": 2,
      "editMinRows": 3,
      "editMaxRows": 6
    }
  ]
}

Add "minRows", "maxRows" to the field in .widget.json.

{
  "name": "MyExampleInfo",
  "title": "minRows 2, maxRows 4",
  "type": "Info",
  "bc": "myExampleBc",
  "fields": [
    {
      "label": "Custom Field",
      "key": "customField",
      "type": "richText",
      "minRows": 2,
      "maxRows": 4
    }
  ],
  "options": {
    "layout": {
      "rows": [
        {
          "cols": [
            {
              "fieldKey": "customField",
              "span": 24
            }
          ]
        }
      ]
    }
  }
}

Add "editMinRows", "editMaxRows" to the field in .widget.json.

{
  "name": "MyExampleForm",
  "title": "editMinRows 3, editMaxRows 6",
  "type": "Form",
  "bc": "myExampleBc",
  "fields": [
    {
      "label": "Custom Field",
      "key": "customField",
      "type": "richText",
      "editMinRows": 3,
      "editMaxRows": 6
    }
  ],
  "options": {
    "layout": {
      "rows": [
        {
          "cols": [
            {
              "fieldKey": "customField",
              "span": 24
            }
          ]
        }
      ]
    }
  }
}

The defaults for all richText fields are frontend constants richTextMinRows, richTextMaxRows, richTextEditMinRows, richTextEditMaxRows:

ui/src/fields/RichText/constants.ts

Live Sample · GitHub

Formatting

Live Sample · GitHub

The user formats the text with the toolbar. When the field is narrow, the buttons that do not fit go to the ⋯ menu. Undo, Redo and Clear Format work for all formatting below.

The value is stored as Markdown, the same format that Yandex Wiki uses. Markdown cannot express some combinations of formatting, and Yandex Wiki fails in these cases too.

✅ supported, ⚠️ supported with a caveat, ❌ not supported, — not checked.

Basics

# Feature Toolbar Status Yandex Wiki Stored as
1 Bold Bold ✅ ✅ **bold**
2 Italic Italic ✅ ✅ *italic*
3 Underline Underline ✅ ✅ ++under++
4 Strikethrough Strikethrough ✅ ✅ ~~strike~~
5 Inline code Code: Inline code ✅ ✅ `code`
6 Text color Text color: Gray, Yellow, Orange, Red, Green, Blue, Violet; Default removes the color ✅ ✅ {red}(text)
7 Link no button, type it in the Markdown markup ✅ ✅ [text](url)
8 Heading H1–H6 Heading: Heading 1 … Heading 6; Text makes a paragraph ✅ ✅ # Head … ###### Head
9 Bullet / ordered list, nested list List: Bullet List, Ordered List, Sink Item, Lift Item ✅ ✅ - item / 1. item
10 Blockquote Quote ✅ ✅ > quote
11 Code block Code: Code block ✅ ✅ ```\ncode\n```
12 Paragraph (Enter) / line break (Shift+Enter) keyboard ✅ ✅ a\n\nb / a \nb
13 Text starting with 4 spaces or a tab stays plain text keyboard ✅ — text stays text

Combinations & overlaps

# Feature Status Yandex Wiki Stored as
14 Two+ styles combined ✅ ✅ ***x***, {red}(**x**)
15 Styles inside heading / list / quote ✅ ✅ # **b** head, - {red}(c) item
16 Three+ styles overlapping in a staircase ✅ ✅ ++abc**def**++**gh~~ij~~**~~kl~~
17 Style across a line break (Shift+Enter) ✅ ✅ **a** \n**b**
18 Bold and italic overlapping each other ⚠️ — an invisible separator is inserted where they meet
19 Bold or italic touching a parenthesis ❌ ❌ abc*def)*ghi cannot be written in Markdown; the toolbar disables such formatting

Text color

# Feature Status Yandex Wiki Stored as
20 Color over text with parentheses ✅ ✅ {red}(Hello \(world\))
21 Color across line breaks (Shift+Enter) ✅ ✅ {red}(a) \n{red}(b)
22 Color on inline code ⚠️ — impossible, inline code excludes other styles

Restriction

  • 13 - a line that starts with 4 spaces or a tab is not turned into a code block. Use Code block for code.
  • 18 - where bold and italic overlap, the editor adds an invisible character between them, so both are kept.
  • 19 - bold, italic, underline, strikethrough and inline code are disabled for a selection that starts or ends at a parenthesis next to a letter, for example )def in abc)def. Text color works.
  • 22 - inline code cannot have a color.