MV3
FakeDOM APITIP
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 channel 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.
await
for results?
When to 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 reference | Main Page referenced object |
---|---|
fakeDom.window | window |
fakeDom.document | window.document |
fakeDom.window.document (alias) | window.document |
fakeDom.document.body | window.document.body |
fakeDom.location | window.location |
fakeDom.window.location (alias) | window.location |
fakeDom.navigator | window.navigator |
fakeDom.window.navigator (alias) | window.navigator |
fakeDom.localStorage | window.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 attributevalue
: 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 calledarguments
: 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:
- Similar to EventTarget.addEventListener API, but there are a few quirks that you should know about.
- 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
- The callback function will receive a
properties
: same as the original ones, with one addition:{ preventDefault: true/false/Object }
: because the callbacks are triggered asynchronously, the only way to preventDefault() an event is to set this property when you register the event. Sadly, this does not offer as much flexibility as you might want, but it is what it is.- If you pass
true
, all of the future triggers of this event will call preventDefault() - If you want to call preventDefault() based on a property of the event, you can do so by passing an object with the properties and values like so:
{ preventDefault: { code: 'KeyX' } }
- this will call preventDefault() for KeyboardEvents that have thecode: 'KeyX'
property (when you pressX
on keyboard).{ preventDefault: { code: ['KeyX', 'KeyY'] } }
- you can also specify an array of multiple values if you want to match any of them. In this example, the event will be prevented if you press either X or Y key.{ preventDefault: { code: ['KeyX', 'KeyY'], shiftKey: true } }
- if you specify multiple properties, the event will be prevented if all properties match. In this example, the event is prevented if you press either Shift + X or Shift + Y combinations.
- If you pass
- Returns:
FakeDomElementPromise
- Usage:
element.addEventListener("click", function(ev) { }, { capture: true, preventDefault: true })