FakeDOM API MV3

TIP

FakeDOM API is only available for Fake Data versions implemented with MV3. If you are not sure which version you are using, please see this table.

If you are using a MV2 version and looking to manipulate DOM elements, please go to this page.

Introduction

In the latest versions of Fake Data that are running under MV3, the custom code that is run by Custom Generators or Loaded Libraries is executed in a sandboxed iframe. This means that there is no direct access to DOM elements in the page from the code that you wrote.

In order to work around this limitation, Fake Data provides an API that facilitates working with DOM elements as easy as possible.

The way this API works is by creating proxy objects that hold a weak reference to the real DOM elements that they reference. It might sound confusing at first, but it's not, you'll see.

A very quick look on how to query elements

Let's take the following js example that returns the element with id="test" attribute.

document.getElementById('test'); // this would normally return an Element object

In order to get a reference to the same element in Fake Data, we would write the following code:

await fakeDom.document.getElementById('test'); // this returns a FakeDomElement object

One of the first things that you might notice is how similar the syntax is. The core idea of creating the FakeDOM API was to offer users a way to work with DOM elements as seamless as possible.

The second thing that you can observe, is the fakeDom object. This is an instance of the FakeDom class, which will act as an entry point for interacting with your DOM elements. The variable is already created for you, so you don't have to worry about instantiating it. You just use it.

The third thing from the above example, is the await keyword. Depending on what you are going to do with the result, it might be or might be not necessary. Without it, the result of that query statement would have been a FakeDomElementPromise object. This is a class that extends a Promise but with a few extra cool features. More about this later in this tutorial.

FakeDomElement class

In the quick look example presented above, it was mentioned that the result from that code is a FakeDomElement object. These objects hold a weak references to a DOM element outside the sandbox. To keep it simple, each instance of this class can hold only one reference to an element from the page.

The way each element communicates with the outside page is by sending messages back and forth through a message channelopen in new window opened between the sandboxed iframe and the main page. Taking advantage of the Channel Messaging API offers a reliable way of communication between the two components (iframe and outside page), but it also has a few downsides that you should keep in mind:

  • It is asynchronous. This means that almost any interaction with a FakeDomElement will return a Promise. You should keep this in mind that, when you want to read a value of an element, you will have to use await.
  • Only primitive data and serializable objects can be sent through it. This means that any reference to an actual object will be lost.

Using in real world

Having this said, let's take a look on how to interact with a FakeDomElement object by looking at a few examples.

Let's say you want to change the value of an input field and then display it in the console:

// This is how you would normally do it through native js
let input = document.getElementById('input'); // returns an Element object
input.value = 'This is a test';
console.log(input.value); // prints "This is a test"

/* --------------------------------------------------------------------------------------------- */

// This is how you do it through FakeDOM API
let input = fakeDom.document.getElementById('input'); // returns a FakeDomElementPromise object
input.value = 'This is a test';
console.log(await input.value); // prints "This is a test"

You can see that even if the first statement returned a Promise, or a FakeDomElementPromise to be more exact, we did not have to await for it before setting the value. That's because all these proxy methods exposed by FakeDomElementPromise and FakeDomElement classes are chainable. The only time when we had to use await was when we were printing the value to the console.

FakeDomElementPromise class

Because all the interaction with DOM elements is done asynchronously, pretty much every method or access to an attribute of a FakeDomElement object will return a promise. This promise is implemented by the FakeDomElementPromise class which is implemented like this:

class FakeDomElementPromise extends Promise {}

Having the class declared like this means that you can use all the functionality that a normal Promise has to offer, as well as the additional functionality that the custom class implements. One of these features is chaining methods whenever is possible.

Let's take a look at the following example:

fakeDom.document.querySelector('body')
				.getElementById('test-input')
				.addEventListener('change', function(e) {
					console.log('input changed');
				});

The above code is an example of method chaining implemented by FakeDomElementPromise class. Every method call there will normally return a FakeDomElementPromise instance, but you can already call the next method immediately without having to await for the response.

However, in background each the above code will be translated to the following:

let tmp;

tmp = await fakeDom.document.querySelector('body');
tmp = await tmp.getElementById('test-input');
tmp = await tmp.addEventListener('change', function(e) {
	console.log('input changed');
});

So we can conclude that this feature will not necessarily speed up the execution, but it will speed up the code writing since it becomes more similar and natural when it compares to the real DOM methods.

TIP: Make sure to check for errors

The last line from the above code will throw an error if there is no element with the id="test-input" attribute on the page, since the second promise will be resolved to a null object.

When to await for results?

There are still scenarios where you will not be able to avoid awaiting for the results. Here are a few examples:

1) When you expect an array (or collection) of elements to be returned

Methods like querySelectorAll or getElementsByClassName will normally return a collection of elements. In FakeDOM API, these will return, after the promise is finished, an array of FakeDomElement objects. Let's look at an example:

(await fakeDom.document.querySelectorAll('input'))
	.forEach(el => { /* ... */ });

In order to be able to loop through the returned elements, you must first wait for the elements to be returned, therefore you will have to use await on the querySelectorAll() method.

2) When you want to retrieve and save the attribute value of an element

If you want to print to the console a value of an attribute, or want to store it to a variable for later use, you will have to await for it.

console.log(await fakeDom.document.title);

Even getters will return an FakeDomElementPromise instance, so you have to await for them in order to get the actual value.

3) When you just want to make sure that an action has been executed

Let's see the following example:

fakeDom.document.body.addEventListener('click', function() {
	console.log('body clicked');
});
fakeDom.document.body.click();

With the above code, we first attach a click event listener on the <body> element, and immediately after that we trigger a click() on it. Because both actions are executed asynchronously, there is no guarantee that the console.log will be triggered this fast.

If we want to be 100% sure that the event callback will be executed even on the immediate click action, we will have to await when attaching the event listener:

await fakeDom.document.body.addEventListener('click', function() {
	console.log('body clicked');
});
fakeDom.document.body.click();

fakeDom global object

fakeDom object is your entry point in using the new FakeDOM API. This variable is declared globally for you so you don't have to worry about it. This object is an instance of the FakeDom class.

While using this API, you may find useful the following references to main page DOM elements:

FakeDom referenceMain Page referenced object
fakeDom.windowwindow
fakeDom.documentwindow.document
fakeDom.window.document (alias)window.document
fakeDom.document.bodywindow.document.body
fakeDom.locationwindow.location
fakeDom.window.location (alias)window.location
fakeDom.navigatorwindow.navigator
fakeDom.window.navigator (alias)window.navigator
fakeDom.localStoragewindow.localStorage
fakeDom.window.localStorage (alias)window.localStorage

Each of the FakeDOM references listed above is an instance of FakeDomElement class.

FakeDomElement class additional methods

This class exposes many methods and properties that have the same name as the real DOM Element methods, such as getElementsById, getAttribute, click, blur, addEventListener, and many others.

However, since every method needs to be implemented individually and since the standards always change and new APIs are constantly added to browsers, the list of exposed functions will not be up-to-date. For these scenarios there are a few fallback methods that you will find helpful:

getDOMElementAttributeValue

  • Details:
    • Retrieves the value of an element attribute.
  • Arguments:
    • attr: the name of the attribute
  • Returns: FakeDomElementPromise
  • Usage:
    • element.getDOMElementAttributeValue("scrollLeft")

setDOMElementAttributeValue

  • Details:
    • Sets the value of an element attribute.
  • Arguments:
    • attr: the name of the attribute
    • value: the value to be set
  • Returns: FakeDomElementPromise
  • Usage:
    • element.setDOMElementAttributeValue("scrollLeft", 500)

callDOMElementMethod

  • Details:
    • Calls a method on a DOM element with given arguments.
  • Arguments:
    • method: the name of the method to be called
    • arguments: an array with the arguments that should be passed. The arguments will be serialized before passing
  • Returns: FakeDomElementPromise
  • Usage:
    • element.callDOMElementMethod("getElementsByClassName", ["btn"])




Other methods that you should know about:

addEventListener

  • Details:
  • Arguments:
    • event_name: the name of the event to listen (nothing caned here)
    • callback_function: a function that will be executed when the event happens - the callback function is executed in the sandboxed iframe, not in the main page context, and is executed asynchronously
      • The callback function will receive a FakeDomEvent object
    • properties: same as the original ones, with one addition:
      • { preventDefault: true/false }: because the callbacks are triggered asynchronously, the only way to preventDefault() an event is to set this to true when you will register the event. Sadly, this does not offer as much flexibility as you might want, but it is what it is.
  • Returns: FakeDomElementPromise
  • Usage:
    • element.addEventListener("click", function(ev) { }, { capture: true, preventDefault: true })