Create an Odoo 14 Markdown Widget Field with TDD - Part 2
Tags: tdd, javascript, odoo, tutorial
Published: 2021-05-12 06:11:28

---
title: Create an Odoo 14 Markdown Widget Field with TDD - Part 2
published: true
date: 2021-05-12 05:10:03 UTC
tags: TDD,JavaScript,Odoo,Tutorial
canonical_url: https://codingdodo.com/create-odoo-markdown-widget-field-with-tdd-part-2/
series: Odoo 14 Javascript Test-Driven Development
---

## Introduction
 ![Create an Odoo 14 Markdown Widget Field with TDD - Part 2](https://codingdodo.com/content/images/2021/05/post-cover-tdd-part-2.png)

This is the **second part** of an article series where we use TDD to develop an Odoo markdown widget.

[Create an Odoo 14 Markdown Widget Field with TDD - Part 1](https://codingdodo.com/create-odoo-markdown-widget-field-with-tdd-part-1/)

In the last part (code available [here](https://github.com/Coding-Dodo/web_widget_markdown/tree/web-widget-markdown-tutorial-part-one)) we ended up with a functional widget transforming pure text markdown content into HTML in render mode and behaving like a standard FieldText when in edit mode.

In this tutorial, we are going to use SimpleMDE Editor instead of the standard FieldText `<textarea>` input.

## Refactoring and adding new tests

First of all, we are going to **remove the test** named `web_widget_markdown edit form`. As a reminder, this test was used to Edit the form and write into the input like that:

```javascript
await testUtils.form.clickEdit(form);
await testUtils.fields.editInput(form.$('.o_field_markdown'), ' **bold content**');
await testUtils.form.clickSave(form);
```

The problem is that the `editInput` function will not work anymore because SimpleMDE will replace the whole `<textarea>` with his own editor and writing inside will not be possible.

### How to test SimpleMDE presence 

To test for SimpleMDE presence we have to analyze how this library insert its editor into the DOM, and a quick inspect gives us more info:

```xml
<div class="CodeMirror cm-s-paper CodeMirror-wrap">
	<div style="overflow: hidden; position: relative; width: 3px; height: 0px; top: 15px; left: 38.8281px;" data-children-count="1">
    	<textarea autocorrect="off" autocapitalize="off" spellcheck="false" tabindex="0" style="position: absolute; padding: 0px; width: 1000px; height: 1em; outline: none;">
        </textarea>
    </div>
    <div class="CodeMirror-vscrollbar" cm-not-content="true" style="bottom: 0px; width: 12px; pointer-events: none;">
    ...
   	</div>
</div>
```

As we can see, SimpleMDE uses the underlying library [CodeMirror](https://codemirror.net/) to create his editor. So checking for the presence of the `div` with class `.CodeMirror` should validate the presence of the Editor. **Let's write a new test.**

```javascript
QUnit.test('web_widget_markdown SimpleMDE is present', async function(assert) {
    assert.expect(1);
    var form = await testUtils.createView({
        View: FormView,
        model: 'blog',
        data: this.data,
        arch: '<form string="Blog">' +
                '<group>' +
                '<field name="name"/>' +
                    '<field name="content" widget="markdown"/>' +
                '</group>' +
            '</form>',
        res_id: 1,
    });
    await testUtils.form.clickEdit(form);
    assert.strictEqual(
        form.$('.o_field_markdown').find("div.CodeMirror").length, 
        1, 
        "CodeMirror div should be present"
    )
    form.destroy();
});
```

### How to test that SimpleMDE is working

To test that simple MDE is working we should:

- First, go into Edit mode so SimpleMDE is initialized (previous test)
- Check that the initial value of our model (data) is passed to SimpleMDE
- Change the value of SimpleMDE content (mock behavior of user writing inside the WYSIWYG) and verify that Odoo widget value has been updated
- Save the Form and assert that our edits are saved and present

#### Writing the tests

To go through our test we will need to **have access to the widget itself** from the mocked FormView. Form object have a `renderer` attribute that will be helpful in that situation by inspecting it's `allFieldWidgets` property:

```javascript
// [1] because in our form the first field is for the name of the blog
// So the first field is in [0] and ours is in [1]
var markdownField = _.find(form.renderer.allFieldWidgets)[1];
```

Inside the test, we want to be able to have access to the **SimpleMDE instance directly from the widget**.

Often times, **we write tests that drive us to implement the solution in a specific way**. In this example we know that we want the Widget Object to hold a property object named `simplemde` containing the current instance of `new SimpleMDE` Editor. This will help us to initialize it, destroy it, set, or get its value. This is a powerful way of programming because the test help us make more robust APIs by directly needing us to implement the strict necessary functions for it to be functional.

So given the idea, we have that property available the test can be written like that

```javascript
QUnit.test('web_widget_markdown edit SimpleMDE', async function(assert) {
    assert.expect(4);
    var form = await testUtils.createView({
        View: FormView,
        model: 'blog',
        data: this.data,
        arch: '<form string="Blog">' +
                '<group>' +
                '<field name="name"/>' +
                    '<field name="content" widget="markdown"/>' +
                '</group>' +
            '</form>',
        res_id: 1,
    });
    await testUtils.form.clickEdit(form);
    var markdownField = _.find(form.renderer.allFieldWidgets)[1];

    assert.strictEqual(
        markdownField.simplemde.value(), 
        "# Hello world", 
        "Initial Value of SimpleMDE should be set"
    )

    markdownField.simplemde.value(' **bold content**');
    assert.strictEqual(
        markdownField._getValue(), 
        " **bold content**", 
        "If we change value in SimpleMDE, value of odoo widget should be updated"
    )

    await testUtils.form.clickSave(form);
    assert.strictEqual(
        form.$('.o_field_markdown').find("strong").length, 
        1, 
        "After Save, b should be present"
    )
    assert.strictEqual(
        form.$('.o_field_markdown strong').text(), 
        "bold content", 
        "After Save, <strong> should contain 'bold content'"
    )
    form.destroy();
});
```

We cannot properly interact with the CodeMirror editor with JQuery testUtils so we will refer to the [CodeMirror user manual](https://codemirror.net/doc/manual.html) to see how to insert a value (this is also what happens when user type) and this is how we will do it from the test function:

```javascript
markdownField.simplemde.codemirror.setValue(' **bold content**');
```

And to test that the **Odoo field** itself has the same value as the Markdown editor we make this assertion.

```javascript
assert.strictEqual(
    markdownField._getValue(), 
    " **bold content**", 
    "Value of odoo widget should be updated"
)
```

#### General knowledge: Understanding \_getValue() in Odoo Widget

`_getValue()` is a function first defined in the `DebouncedField` _(FieldText inherits DebouncedField)_.

```javascript
// Inside DebouncedField in odoo/addons/web/static/src/js/fields/basic_fields.js
/**
 * Should return the current value of the field, in the DOM (for example,
 * the content of the input)
 *
 * @abstract
 * @private
 * @returns {*}
 */
_getValue: function () {},
```

A `DebouncedField` is a superclass that handles the debouncing of the user input.

> Debouncing an input in JavaScript is a common technique to reduce the rate of execution of a function. If a user is typing inside an input and you execute a function on each change of that input (each letter typed) it can quickly lead to a lot of computation power being used on just that. The common technique is called debounced and it will delay the execution of the function listening to the input, only every X secondes. Odoo use `_.debounce` for this with the [underscorejs library](https://underscorejs.org/#debounce).

**This is a**  **summarized view of the Odoo Fields Widget inheritance graph**

```javascript
// the super class
var AbstractField = {}
	// handle debouncing
	var DebouncedField = AbstractField.extend({})
    	// handle keystroke evnts, state and other things
		var InputField = DebouncedField.extend({})
        	// more specific implementations using InputField logic
			var FieldText = InputField.extend({})
			var FieldChar = InputField.extend({})
            var FieldDate = InputField.extend({})
            var FieldDate = InputField.extend({})
```

Most of all the field inheriting InputField are overriding this `_getValue()` function to return more than the basic `this.value` property of a widget and we will do the same.

Running the tests in the current state of our widget expectedly fail.

![Create an Odoo 14 Markdown Widget Field with TDD - Part 2](https://res.cloudinary.com/phildl-cloudinary/image/upload/v1617185184/codingdodo/tdd-markdown-widget-part-two-error-tests.png)

## Initialize SimpleMDE editor in widget Edit mode

As we wrote our tests earlier, we know that we need to have `simplemde` as a property of our widget, let's then extend the `init` function of our widgetto do so:

```javascript
/**
* @constructor
*/
init: function () {
    this._super.apply(this, arguments);
    this.simplemde = {}
},
```

### Attaching SimpleMDE to our Widget dom root element.

And in the `start` function _(available in all Odoo Widgets)_ we will do this:

```javascript
/**
 * When the the widget render, check view mode, if edit we
 * instanciate our SimpleMDE
 * 
 * @override
 */
start: function () {
    if (this.mode === 'edit') {
        this.simplemde = new SimpleMDE({element: this.$el[0]});
    }
    return this._super();
},
```

When we instantiate SimpleMDE we need to at least give him the `element` option or else it will attach itself to any `<textarea>` existing _(this the the default behavior of the library)_.

**What is `this.$el[0]` ?**

`this.$el` is a JQuery object and **not a pure dom** Element as required by SimpleMDE, so by doing `this.$el[0]` we get the proper dom element.

Keep in mind that we inherit FieldText, and FieldText has some original logic about the HTML element it uses to render itself. In read-only mode, it is a `<span>` and in edit mode the tag change, as seen here in the <u>source code</u> of the `FieldText` :

```javascript
/**
* @constructor
*/
init: function () {
	this._super.apply(this, arguments);

	if (this.mode === 'edit') {
		this.tagName = 'textarea';
	}
}
```

### Running the tests and analyzing the error

Now if we run the tests we will see this error

1. **Cannot read property 'insertBefore' of null@ 121 ms**

```shell
TypeError: Cannot read property 'insertBefore' of null
    at http://localhost:8069/web_widget_markdown/static/lib/simplemde.min.js:12:1240
    at new t (http://localhost:8069/web_widget_markdown/static/lib/simplemde.min.js:7:31640)
    at new e (http://localhost:8069/web_widget_markdown/static/lib/simplemde.min.js:7:29476)
    at e (http://localhost:8069/web_widget_markdown/static/lib/simplemde.min.js:7:29276)
    at Function.e.fromTextArea (http://localhost:8069/web_widget_markdown/static/lib/simplemde.min.js:12:1213)
    at B.render (http://localhost:8069/web_widget_markdown/static/lib/simplemde.min.js:15:4157)
    at new B (http://localhost:8069/web_widget_markdown/static/lib/simplemde.min.js:14:28861)
    at Class.start (http://localhost:8069/web_widget_markdown/static/src/js/field_widget.js:34:30)
    at Class.prototype.<computed> [as start] (http://localhost:8069/web/static/src/js/core/class.js:90:38)
    at http://localhost:8069/web/static/src/js/core/widget.js:440:25
```


The error actually comes **from the simplemde library** trying to insert itself into the DOM. We gave him `$el[0]` as an element. And as seen in the source code, the actual element given is a `<textarea>`, this is due to us inheriting FieldText.

But the problem actually comes from the surrounding of the `<textarea>` element. **SimpleMDE will actually use** `parentNode` **on the element given to place itself.** The element given as `$el[0]` as is has **no parent** due to the way the Odoo Framework inserts it in the DOM.

So the base **template** of our field cannot be as simple as a span, it has to be encapsulated by another div or something else.

### Moving to a dedicated Qweb Template for our widget

To create a Template for a widget we need to create an XML file containing our template then explicitly use it in our javascript widget declaration.

#### The Qweb template

Create the file `static/src/xml/qweb_template.xml` with this content.

```xml
<?xml version="1.0" encoding="UTF-8"?>
<templates>
    <t t-name="FieldMarkdown">
        <div class="o_field_markdown">
            <textarea name="o_field_markdown" id="o_field_markdown"></textarea>
        </div>
    </t>
</templates>

```

We gave our template the same name `t-name="FieldMarkdown` as the name we export in our JavaScript file for consistency.   
Inside it is just a wrapper div class with the same class `.o_field_markdown` we used before and inside it a `<textare>` for SimpleMDE to attach to.

Add it to your ` __manifest__.py`

```python
"qweb": [ 
	'static/src/xml/qweb_template.xml',
],
```

#### Using the template in our JavaScript field widget

```javascript
var markdownField = basicFields.FieldText.extend({
    supportedFieldTypes: ['text'],
    // className: 'o_field_markdown',
    template: 'FieldMarkdown', // name of template in xml Qweb file
    jsLibs: [
        '/web_widget_markdown/static/lib/simplemde.min.js',
    ],
    // ...
```

We removed the `className` attribute because it is no longer useful.

Run the tests again and surely it fails again because we still tell SimpleMDE to attach itself to the **root** $el of our widget.

#### Refactoring our widget to use the new template

Inside the start function of the widget, we will target the `<textarea>` **inside** the `<div>` we created in the template.

```javascript
start: function () {
    if (this.mode === 'edit') {
        var $textarea = this.$el.find('textarea');
        this.simplemde = new SimpleMDE({element: $textarea[0]});
    }
    return this._super();
},
```

Now if we run the tests again:   
- Markdown Widget Tests: web\_widget\_markdown SimpleMDE is present **(1)**  **✅**  
- Markdown Widget Tests: web\_widget\_markdown edit SimpleMDE (3, 0, 3) ❌

It means **our SimpleMDE is well initialized** but there is no communication of value between the widget and SimpleMDE editor.

## Communication between SimpleMDE and the widget

#### Initialize SimpleMDE with data value

The first test we will try to pass is _Initial Value of SimpleMDE should be set._ To do so, we will refer to the [SimpleMDE documentation on setting and getting value](https://github.com/sparksuite/simplemde-markdown-editor#getset-the-content).

We see that there is a simple method `set("value")` but also an `initialValue` that can be passed at instantiation. We will choose the second solution and make these changes to the `start` function of our widget:

```javascript
start: function () {
    if (this.mode === 'edit') {
        var $textarea = this.$el.find('textarea');
        this.simplemde = new SimpleMDE({
            element: $textarea[0],
            initialValue: this.value, // this.value represents widget data
        });
    }
    return this._super();
},
```

**Now we run the tests again and surely see that our first test passed ✅**

In the first part, we handled the `_renderReadonly` function, now that we work on edit mode we will override the function `_renderEdit` to set the value into SimpleMDE, add these methods to the widget

```javascript
    _formatValue: function (value) {
        return this._super.apply(this, arguments) || '';
    },

    _renderEdit: function () {
        this._super.apply(this, arguments);
        var newValue = this._formatValue(this.value);
        if (this.simplemde.value() !== newValue) {
            this.simplemde.value(newValue);
        }
    },
```

SimpleMDE **can't handle false or null value** so the function `_formatValue` is there to help us return an empty string when there is nothing in the field.

<!--kg-card-begin: html-->
`_renderEdit` and `_renderReadonly` are called by the main `_render` function that is defined in `odoo/addons/web/static/src/js/fields/abstract_field.js`. This main render function handles the conditional logic of the widget being in Edit or Readonly mode and call the correct function: 
```javascript

    	
    _render: function () {
        if (this.attrs.decorations) {
            this._applyDecorations();
        }
        if (this.mode === 'edit') {
            return this._renderEdit();
        } else if (this.mode === 'readonly') {
            return this._renderReadonly();
        }
    },
	    
    
```

<!--kg-card-end: html-->

Again we run the tests and everything is still green ✅ so we can go to the next step.

### Listening to change in SimpleMDE to update our widget value.

In our previous test, we wrote that `markdownField._getValue()` should be equal to what we write inside the SimpleMDE editor.

Naturally we will add that `_getValue()` function and make it return the inner value of SimpleMDE.

```javascript
/**
 * return the SimpleMDE value
 *
 * @private
 */
_getValue: function () {
	return this.simplemde.value();
},
```

Since we have access to the property `simplemde` that we initialize in our widget it is very easy to get the data.

Then, to listen to changes, we have to get the CodeMirror instance of our SimpleMDE and listen to its `change` events that CodeMirror is triggering.

```javascript
start: function () {
    if (this.mode === 'edit') {
        var $textarea = this.$el.find('textarea');
        this.simplemde = new SimpleMDE({
            element: $textarea[0],
            initialValue: this.value,
        });
        var self = this;
        this.simplemde.codemirror.on("change", function(){
            self._setValue(self.simplemde.value());
        })
    }
    return this._super();
},
```

_We had to declare `var self = this` to be able to use it in the callback function._

With that change made let's run the tests again

1. The initial Value of SimpleMDE should be set@ 75 ms ✅
2. If we change the value in SimpleMDE, the value of the odoo widget should be updated@ 81 ms ✅
3. After Save, b should be present@ 380 ms ✅
4. After Save, <strong> should contain 'bold content' ✅

**Victory !**

Now that our tests passed we can try the module from the user perspective

![Create an Odoo 14 Markdown Widget Field with TDD - Part 2](https://res.cloudinary.com/phildl-cloudinary/image/upload/v1617616816/codingdodo/testing-the-ui.png)

Unfortunately, we can see that there is some problem with the aspect of our Markdown Editor.

It seems that the height is fixed so there is not enough space for it. This is coming from the fact that we are extending the **FieldText widget and it has built-in auto-resize features**.

In the next part, we will see how to deal with that as we improve our widget.


The source code for this tutorial [is available here on GitHub](https://github.com/Coding-Dodo/web_widget_markdown/tree/web-widget-markdown-tutorial-part-two).

✨ Update 17/06/2021 🎓 [Third part is now available here](https://codingdodo.com/create-odoo-markdown-widget-field-with-tdd-part-3)

Thanks for reading, if you liked this article please consider:

- ☕️ [Buying me a Coffee](https://www.buymeacoffee.com/CodingDodo)
- 🥳 Register on [Codingdodo.com](https://codingdodo.com)