Intercept Interface Changes with Mutation Observer

DayBack exposes many triggers that allow you to intercept and modify DayBack's behavior using App Actions and Event Actions. There are cases, however, where you need to modify DayBack's user interface before, or immediately after an interface element is drawn and displayed to the user.

Examples of this may include modifying the Event Popover or sidebar right after a user opens them. Other examples may include waiting for the opening or closing of a utility panel, or the opening of a dropdown menu.

You can watch for these types of events using DayBack's dbk.observe() helper function. This function is part of a collection of available functions you can call in any app action. The dbk.observe() function can watch for changes to the specific elements in the DOM, and then trigger custom code when certain criteria are met.


Watching for Changes

You can watch a specified element for changes that occur to that element. An example would be watching the calendar element for the edit event popover to appear. Once a specific condition is true, you can run custom code. Starting an observer requires four parameters:

let observer = dbk.observe({
    name:   "uniqueNameForThisObserverInstance",
    watch:  document.getElementById("The Dom Element We are Observering"),
    until:  ".query-selector .condition-that-triggers .custom-code-injection",
    then:   functionToRunWhenConditionIsMet
});

function functionToRunWhenConditionIsMet(observer) {

    // The most important functions inside observer are

    observer.stop();        // Stops further observation, if you only need
                            // to run the first time something happens

    observer.destroy();     // Stops further observation, and destroys
                            // named observer, in case you want to reuse
                            // the same name in the future. 

    observer.restart();     // If you expect your injection to go away, 
                            // you can restart observeration on the same
                            // node. 

    // You also have access to the entire node tree and the last
    // mutation that triggered the runtime condition

    observer.lastMutation;

    observer.mutationList;

    // If you used a query selector for your 'until' clause, and you want
    // to retreive it, you can reference it using the foundNode parameter

    observer.foundNode;
}

Example Scenario:

As a simple example, you could watch the sidebar for the text filter to be rendered, and then change its background color to red. To do this simple change, you can add the following code to an After Calendar Rendered action.

// Change Text Filter in sidebar to have a red background

let observerInstance = dbk.observe({
    name: "modifySidebar",
    watch: document.getElementById("sidebar"),
    until: "#sidebar .filters-popover-container text-filter .header-block-content",
    then: runCustomCode,
});

function runCustomCode(observer) {

    // Check to make sure we haven't made this modification already

    if (!observer.foundNode.classList.contains("observerModified")) {

        // Stop observing the filter for new mutations, as we are
        // going to add our own mutations

        observer.stop();

        // Add a CSS class to the node, indicating we've modified it

        observer.foundNode.classList.add("observerModified");

        // Change the background color

        observer.foundNode.style.backgroundColor = "red";

        // Start observering the sidebar again, since our modification
        // may disappear if the user closes and reopenes the sidebar.

        observer.restart();
    }
}        

Configuration Options

Here are the basic configuration parameters for the dbk.observe() function:


name: "uniqueName"

Specify a unique short string that identifies the unique observer instance. Naming your observers prevents the DayBack from starting more than one observer with the same name. This could potentially happen if you start observers from multiple app actions or event actions that may run more than one time in a row. Calling observer.destroy() will clear out a previously created instance, and will allow you to create a new observer using the same name in the future.


watch: querySelector string literal or node object

Specify the DOM element that is being observed. This has to be a querySelector string or a valid node object obtained from document.querySelector() or document.getElementById() call:

let observerInstance = dbk.observe({
    name: "modifySidebar",
    watch: "#sidebar" // or document.querySelector('#sidebar')
    until: "#sidebar .filters-popover-container text-filter .header-block-content",
    then: runCustomCode,
});

If you specify a node, the function will immediately start watching that node for the stop condition specified in the until parameter.

If you specify a querySelector string instead, the dbk.observe() function will first check to see if the node exists in the DOM. If found, it will start observing that node immediately. If the element does not exist, the observer will first observe the root calendar node (#calendar) and wait for the creation of the node specified by the querySelector string. When the node is found, it will then start observing that node for the stop condition.

Using string literals allows you to start your mutation observers before the calendar is fully loaded, as the observed node doesn't need to exist before observer instantiation. There is no difference in using this method, versus using a nested sequence of observers that first observe the calendar node for a sub-node, and subsequently start a new observer on this sub-node. The use of a querySelector string is simply a convenient way to reduce the amount of observer objects you need to create.


until: querySelector string literal or conditionCheckFunction

Specify the stop condition you want to watch for. The most common stop condition would be a query selector returning true, which indicates that DayBack has added your desired object to the DOM. An alternative would be to specify a custom function that returns true when a stop condition is true. You could use this to check whether an event or editEvent object contains a certain value that constitutes a valid stop condition.


then: functionToRun

The function will be passed the observer object as its only parameter. The object contains the configuration collection including the stop, start, restart, and destroy functions as well as the last matched mutation (observer.foundNode).

Optional Runtime Parameters

The following parameters provide advanced flow control for your mutation observer and can be optimized to reduce CPU cycles and tweak mutation observer performance. All of these have defaults, so you may not need to use these settings.


checkStopConditionOnStart: true

By default, the observer will immediately run the until stop condition check when it is started. There may be cases where the element is already present in the DOM, which means we do not need to wait and can run the custom code immediately. Alternatively, you may wish to start your observer, and then trigger some DayBack action by initiating a click event on an on-screen element after you have started the observer. An example of this would be simulating a mouse click on the gear icon in the Event Popover. This would initiate the opening of a utility panel which may then wish to modify in some way. In this case, the on-screen elements aren't drawn yet, but you expect them to be redrawn soon after you initiate the click event.


whenFoundStopObserving: false

By default, if a mutation is found, we will continue to observe for future mutations, unless the stop condition is met and the user calls observer.stop() inside of their then code injection function. The observer.stop() function should always be ran inside of the code injection function if you are injecting code into the node that is being observed. If you don't call observer.stop(), you will trigger an infinite loop of mutation changes. You can always call observer.restart() if you wish to continue to observe the node after you have applied your changes to it, however.


whenFoundStopProcessing: true

Each DOM change will result in an array of all mutations. If we find a valid stop condition, we very likely do not need to loop through the remaining mutations. Leave this set to true to save CPU cycles.


autoStart: true

startDelay: 0

By default, we will start observing right away and will not wait. You can optionally have the mutation observer start after a delay of a set number of milliseconds. Alternatively, you can start your observer manually after you have created it, as follows.


let observer = seedcodeCalendar.get("dbkObserver").new({
    name:   ...,
    watch:  ...,
    until:  ...,
    then:   ...,

    autoStart: false

});        

... do stuff ...

observer.start();

debug: false

If set to true, the observer will print debug statements to the JavaScript console at each major stage of mutation observation.


options: { attributes: false, childList: true, subtree: true}

The options object specifies what type of DOM changes should trigger a condition check. The most common scenario is that we want to monitor change in immediate child nodes, as well as the subtree of nodes. You may only need to change this if you want to observe an attribute of a node rather than the entire childList or subtree.


mutationType: "childList" || "attribute"|| "both"

By default, we will run our condition check on a childList mutation type only, regardless of what was specified in the options object. You can override this by specifying the mutationType that should trigger a stop condition check.


Example Extension that use the Mutation Observer

Here are a few examples from our extensions library illustrating how to use the DayBack mutation observer library to override the user interface.


Add Custom Buttons to Sidebar

This After Calender Rendered app action watches for the opening and closing of the sidebar and for changes to the tabs that are selected when a user navigates to different portions of the sidebar. When the appropriate sidebar condition is true, the action adds several custom buttons and dropdowns.

Add Buttons to the Event Popover

This On Event Click event action watches for the Edit Event popover to load. Once loaded, it modifies the button bar to add custom buttons next to DayBack's standard Delete and Save & Close buttons.