Web Apps

12.1 Observers

# Observers

The Observer APIs allow us to create objects that watch selected parts of the DOM. If changes to the DOM elements we can automatically have a callback function run.

# Resize Observer

The resize observer can be used to duplicate the same functionality as a Media Query, but the real power is being able to watch specific DOM elements instead of the whole page and see if the desired element has changed to meet a dynamic size criteria, then we can do anything we want with JavaScript. We can add or remove elements from the page. We can fetch new content. We can apply new CSS. We can rearrange our page layout entirely.

The basic script works like this:

//create an observer passing in a callback function
let observer = new ResizeObserver(handleResize);
//tell the observer what to watch
observer.observe(document.querySelector('.something'));

//create your callback function
function handleResize(entries) {
  //function will be sent an array of elements being observed
  //each entry has a `target` property that points to the observed element
  let myelement = entries[0].target;
  myelement.className.add('hasChanged');
  //each entry also has a `contentRect` object with width and height properties
  console.log(entries[0].contentRect.width);
  console.log(entries[0].contentRect.height);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

And here is a video that explains the whole process.

MDN reference for Resize Observer (opens new window)

# Intersection Observer

The intersection observer is a very common observer. It is used to create effects on the page as the user scrolls. When an observed element intersects with an area of the screen, it triggers the callback and lets you run your script. Generally, the script will do something like add a css class to trigger a transition or animation. However, it could also do something like fetch new content from a remote API or the Cache.

The intersection observers work in a similar way to the resize observer. You create the observer and then tell it what to watch. There will be a callback function that runs when the intersections occur. The callback function will be pass the array of elements being observed with properties about each that you can use in deciding what you want to do.

The intersection observers need some extra options that you define when creating it. The first option is called root, and it defines the viewport that will be used to watch for intersections with the observed element. The second is rootMargin and lets you expand or shrink your viewport when watching for intersections. The final option is threshold and let's you define a percentage of how much of the observed element must be intersecting with the viewport before calling the callback function.

//set up the options
let opts = {
  root: null, //null means the whole screen. Otherwise it can be another element as the viewport
  rootMargin: '0px -50px', //top-bottom and left-right values.
  //positive means bigger than viewport. negative means inset from edges
  threshold: 0.5, //percentage of observed element inside defined area. 0.5 == 50%
};
//create the observer with the options and callback function
let observer = new IntersectionObserver(handleIntersect, opts);
//tell it what to observe.
observer.observe(document.querySelector('.somediv'));
1
2
3
4
5
6
7
8
9
10
11

If you want to observe many elements then just call observe on each.

There is also an unobserve method that lets you remove an element from the set being observed.

function handleIntersect(entries) {
  entries.forEach((entry) => {
    //for each observed item report if it is currently intersecting
    console.log(entry.isIntersecting); //boolean value
    //use an if statement to do whatever you like
  });
}
1
2
3
4
5
6
7

First video is a basic introduction to building an intersection observer and demonstrates effects on paragraphs as the user scrolls.

The second video shows how you could build an infinite scrolling system that dynamically loads new content as the user scrolls.

MDN reference for Intersection Observer (opens new window)

# Mutation Observer

The Mutation Observer will let you observe DOM elements and watch for changes to their textContent or attributes or children. It can be a useful observer to do things like highlight areas of the page when new content is added, changed, or removed.

Similar to the Intersection Observer, the Mutation Observer needs a set of options.

//set the options
const opts = {
  attributes: true, //report if attributes are changed
  attributeFilter: ['src', 'href'], //optional list of attributes to watch
  attributeOldValue: false; //optional. if true old value will be saved for callback function
  childList: true, //report if children are changed
  characterData: false, //optional. if true, will save the text for the callback function
  characterDataOldValue: false; //optional. if true old text value will be saved for callback function
  subtree: false, //report if elements further down in the descendent tree are changed
  //this last one, pluse the characterData ones can come with a performance hit.
};

//create the observer object with callback function and options
let observer = new MutationObserver(handleMutation, opts);
//add the element(s) you want to watch
observer.observe(document.querySelector('.somediv'));
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

You can add more elements by calling observe() again to add other elements to the observed set. You can always call unobserve() to remove elements from the observed set.

function handleMutation(mutations) {
  //entries is the list of _MUTATED_ observed elements
  //each will have a `type` property that indicates which type of mutation it is
  switch (mutations[0].type) {
    case 'childList':
      //a child element was mutated
      console.log(mutations[0].target);
      //old and new values might be available if set in options
      break;
    case 'attributes':
      //attribute was changed
      console.log(mutations[0].target);
      //we can find out which attribute was mutated
      console.log(mutations[0].attributeName);
      //plus the old and new values if set in options
      break;
    default:
    //subTree mutation
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

MDN reference for Mutation Observer (opens new window)

# JS Classes

JavaScript does not have actual "classes" like languages such as Swift, Kotlin, C++, C#, etc. JavaScript uses prototypes to define how object properties and methods are shared through inheritance.

However, because of 20 years of confusion over how the keyword this works, a desire to standardize the syntax for creating new objects, plus due to the number of developers coming to JavaScript from other languages in the last 10 years, a class keyword was added. A syntactic sugar was added to JavaScript that lets developers create objects with a class-like syntax.

If you want to create an Object in JavaScript there are a number of ways that you can do this.

//an object literal
//just write what you want as the props and use the default values for all property descriptors
let objLiteral = { id: 123, name: 'Steve' };

//Object.create method
//pass in a prototype object and a properties object
let createdObj = Object.create(somePrototypeObject, {
  id: { value: 123, enumerable: true },
  name: { value: 'Steve', enumerable: true },
});

//A constructor function
//when calling a function with `new` the return value will be your new object
//CANNOT do this with an ARROW function
function myBuilder() {
  this.id = 123; //add properties to the object that will be returned
  this.name = 'Steve';
}
let constructedObj = new myBuilder();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

You can then extend the functionality of any object by using the prototype keyword to add methods to the prototype object.

myBuilder.prototype.someNewMethod = function () {};
createdObj.prototype.someNewMethod = function () {};
//with the object literal we need to get to the constructor object to find the prototype object
//two ways to do this. 1. with the `constructor` property
objLiteral.constructor.prototype.someNewMethod = function () {};
// 2. by using the __proto__ as the shortcut for `constructor.prototype`
objLiteral.__proto__.someNewMethod = function () {};
1
2
3
4
5
6
7

In recent years, the class keyword was added to JavaScript as a syntactic sugar. This was an attempt to standardize (yet again) the way that objects are created in JavaScript and to make the language appear more familiar to the many developers migrating to JavaScript from other languages.

class myObjType {
  constructor() {
    this.id = 123;
    this.name = 'Steve';
  }

  someNewMethod() {
    //this method is added to the prototype
    //note the shorthand syntax for defining the function without `function`
  }
}
//create one of your objects, just like with a function plus `new`
let myClassObj = new myObjType(); //calls the constructor() function
//myClassObj will have the two properties - id and name
//the prototype for myClassObj's constructor will hold the `someNewMethod` function
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

By using the class keyword we can build their constructors, define their properties and define their prototype methods in a more predictable way. This does not stop JS using the prototype chain or change how anything happens internally.

You will still use the Object literal syntax for 80%+ of what you do with objects in JavaScript. The class syntax just gives you an alternative standard to follow when things become more complex.

# What to do this week

TODO

Things to do before next week.

  • Read all the content from Modules 12.1, 12.2, and 13.1.
  • Finish the Hybrid Exercises
Last Updated: 5/31/2023, 8:15:38 PM