Mutation Observer API
I need to learn about the Mutation Observer API in order to better implement the embedding of social media posts. Right now, social media posts are wrapped in a wrapper div that has a min-height property set on it to prevent too much cumulative layout shift. I want to remove that min-height from the wrapper div after the embedded post is rendered to make the application more responsive.
References
Notes
The MutationObserver
interface provides the ability to watch for changes being made to the DOM tree. It is designed as a replacement for the older Mutation Events feature, which was part of the DOM3 Events specification.
Constructor
MutationObserver()
- Creates and returns a new
MutationObserver
which will invoke a specified callback function when DOM changes occur. - DOM observation does not begin immediately; the
observe()
method must be called first to establish which portion of the DOM to watch and what kind of changes to watch for.
- Creates and returns a new
new MutationObserver(callback)
- The
callback
parameter above will be called on each DOM change that qualifies given the observed node or subtree and options. - The
callback
function takes two input parameters: - An array of
MutationRecord
objects, describing each change that occurred. - The
MutationObserver
which invoked thecallback
. This is most often used to disconnect the observer usingMutationObserver.disconnect()
.
- An array of
- The
MutationRecord
- The
MutationRecord
is a read-only interface that represents an individual DOM mutation observed by aMutationObserver
. It is the object inside the array passed to the callback of aMutationObserver
.
Instance Properties
MutationRecord.addedNodes
- The nodes added by a mutation. Will be an empty
NodeList
is no nodes were added.
- The nodes added by a mutation. Will be an empty
MutationRecord.attributeName
- The name of the changed attribute as a string, or
null
.
- The name of the changed attribute as a string, or
MutationRecord.attributeNamespace
- The namespace of the changed attribute as a string, or
null
- The namespace of the changed attribute as a string, or
MutationRecord.nextSibling
- The next sibling of the added or removed nodes, or
null
- The next sibling of the added or removed nodes, or
MutationRecord.oldValue
- The value depends on
MutationRecord.type
: - For
attributes
, it is the value of the changed attribute before the change - For
characterData
, it is the data of the changed node before the change - For
childList
, it isnull
- For
- The value depends on
MutationRecord.previousSibling
- The previous sibling of the added or removed nodes, or
null
- The previous sibling of the added or removed nodes, or
MutationRecord.removedNodes
- The nodes removed by a mutation. Will be an empty
NodeList
if no nodes were removed
- The nodes removed by a mutation. Will be an empty
MutationRecord.target
- The node the mutation affected, depending on the
MutationRecord.type
. - For
attributes
, it is the element whose attribute changed - For
characterData
, it is theCharacterData
node - For
childList
, it is the node whose children changed
- For
- The node the mutation affected, depending on the
MutationRecord.type
- A string representing the type of mutation:
attributes
if the mutation was an attribute mutation,characterData
if it was a mutation to aCharacterData
node, andchildList
if it was a mutation to the tree of nodes.
- A string representing the type of mutation:
Instance Methods
MutationObserver.disconnect()
- The
MutationObserver
methoddisconnect()
tells the observer to stop watching for mutations. - The observer can be reused by calling its
observe()
method again.
- The
If the element being observed is removed from the DOM, and then subsequently released by the browser's garbage collection mechanism, theMutationObserver
will stop observing the removed element. However, theNutationObserver
itself can continue to exist to observe other existing elements.
MutationObserver.observe(target, options)
- The
MutationObserver
methodobserve()
configures theMutationObserver
callback to begin receiving notifications of changed to the DOM that match the given options. - Depending on the configuration, the observer may watch a single
Node
in the DOM tree, or that node and some of its descendant nodes. - To stop the
MutationObserver
(so that none of its callback will be triggered any longer), callMutationObserver.disconnect()
- The
observe(target, options);
- Parameters
target
- A DOM
Node
(which may be anElement
) within the DOM tree to watch for changes, or to be the root of a subtree of nodes to be watched.
- A DOM
options
- An object providing options that describe which DOM mutations should be reported to
mutationObserver
'scallback
. At a minimum, one ofchildList
,attributes
, and/orcharacterData
must betrue
when you callobserve()
. Otherwise, aTypeError
exception will be thrown. subtree
- Set to
true
to extend monitoring to the entire subtree of nodes rooted attarget
. All of the other properties are then extended to all of the nodes in the subtree instead of applying solely to thetarget
node. The default value isfalse
.
- Set to
childList
- Set to
true
to monitor the target node (and, ifsubtree
istrue
, its descendants) for the addition of new child nodes or removal of existing child nodes. The default value isfalse
.
- Set to
attributes
- Set to
true
to watch for changed to the value of attributes on the node or nodes being monitored. The default value iftrue
if either ofattributeFilter
orattributeOldValue
is specified, otherwise the default value isfalse
.
- Set to
attributeFilter
- An array of specific attribute names to be monitored. If this property isn't included, changes to all attributes cause mutation notifications.
attributeOldValue
- Set to
true
to record the the previous value of any attribute that changes when monitoring the node or nodes for attribute changes. The default value iffalse
.
- Set to
characterData
- Set to
true
to monitor the specified target node (and, ifsubtree
istrue
, its descendants) for changes to the character data contained within the node or nodes. The default value iftrue
ifcharacterDataOldValue
is specified, otherwise the default value isfalse
.
- Set to
characterOldValue
- Set to
true
to record the previous value of a node's text whenever the text changes on nodes being monitored. The default value isfalse
.
- Set to
- An object providing options that describe which DOM mutations should be reported to
MutationObserver.takeRecords()
- The
MutationObsrver
methodtakeRecords()
returns a list of all matching DOM changes that have been detected but not yet processed by the observer's callback function, leaving the mutation queue empty. - The most common use for this is to immediately fetch all pending mutation records immediately prior to disconnecting the observer, so that any pending mutations can be processed when shutting down the observer.
- Returns an array of
Mutationrecord
objects, each describing one change applied to the observed portion of the document's DOM tree.
- The
Exceptions
TypeError
- Thrown in any of the following circumstances:
- The
options
are configured such that nothing will actually be monitored. (For example, ifchildList
,attributes
, andcharacterData
are allfalse
.) - The value of
options.attributes
is false, butattributeOldValue
istrue
and/orattributeFilter
is present. - The
characterDataOldValue
option istrue
butcharacterData
isfalse
(indicating that character changes are not to be monitored).
- The
Usage Notes
Reusing Mutation Observers
- You can call
observe()
multiple times on the sameMutationObserver
to watch for changes to different parts of the DOM tree and/or different types of changes. There are some caveats to note: - If you call
observe()
on a node that's already being observed by the sameMutationObserver
, all existing observers are automatically removed from all targets being observed before the new observer is activated. - If the same
MutationObserver
is not already in use on the target, then the existing observers are left alone and the new one is added.
- If you call
Social Media Embed Mutation Observers
Note: I added a wrapper <div style="display: flex; flex-direction: row; justify-content: center;"> for each of the social media posts so that they are styled correctly. We will use this wrapper for the mutation observer.
X Posts
- Embedded X Posts initially look similar to below:
<div class="flex-row justify-center" style="min-height: <%=locals.desktop?'762px':'564px'%>;">
<blockquote class="twitter-tweet"><p lang="en" dir="ltr">A melanistic leopard <br> <a href="https://t.co/tXTSGhqVbL">pic.twitter.com/tXTSGhqVbL</a></p>— Science girl (@gunsnrosesgirl3) <a href="https://twitter.com/gunsnrosesgirl3/status/1808192447630942582?ref_src=twsrc%5Etfw">July 2, 2024</a></blockquote>
</div>
- After the
window.twttr.widgets.load();
function is called, embedded X posts look similar to below:
<div class="flex-row justify-center" style="min-height: 762px;">
<div class="twitter-tweet twitter-tweet-rendered" style="display: flex; max-width: 550px; width: 100%; margin-top: 10px; margin-bottom: 10px;"><iframe id="twitter-widget-0" scrolling="no" frameborder="0" allowtransparency="true" allowfullscreen="true" class="" title="X Post" src="https://platform.twitter.com/embed/Tweet.html?dnt=false&embedId=twitter-widget-0&features=eyJ0ZndfdGltZWxpbmVfbGlzdCI6eyJidWNrZXQiOltdLCJ2ZXJzaW9uIjpudWxsfSwidGZ3X2ZvbGxvd2VyX2NvdW50X3N1bnNldCI6eyJidWNrZXQiOnRydWUsInZlcnNpb24iOm51bGx9LCJ0ZndfdHdlZXRfZWRpdF9iYWNrZW5kIjp7ImJ1Y2tldCI6Im9uIiwidmVyc2lvbiI6bnVsbH0sInRmd19yZWZzcmNfc2Vzc2lvbiI6eyJidWNrZXQiOiJvbiIsInZlcnNpb24iOm51bGx9LCJ0ZndfZm9zbnJfc29mdF9pbnRlcnZlbnRpb25zX2VuYWJsZWQiOnsiYnVja2V0Ijoib24iLCJ2ZXJzaW9uIjpudWxsfSwidGZ3X21peGVkX21lZGlhXzE1ODk3Ijp7ImJ1Y2tldCI6InRyZWF0bWVudCIsInZlcnNpb24iOm51bGx9LCJ0ZndfZXhwZXJpbWVudHNfY29va2llX2V4cGlyYXRpb24iOnsiYnVja2V0IjoxMjA5NjAwLCJ2ZXJzaW9uIjpudWxsfSwidGZ3X3Nob3dfYmlyZHdhdGNoX3Bpdm90c19lbmFibGVkIjp7ImJ1Y2tldCI6Im9uIiwidmVyc2lvbiI6bnVsbH0sInRmd19kdXBsaWNhdGVfc2NyaWJlc190b19zZXR0aW5ncyI6eyJidWNrZXQiOiJvbiIsInZlcnNpb24iOm51bGx9LCJ0ZndfdXNlX3Byb2ZpbGVfaW1hZ2Vfc2hhcGVfZW5hYmxlZCI6eyJidWNrZXQiOiJvbiIsInZlcnNpb24iOm51bGx9LCJ0ZndfdmlkZW9faGxzX2R5bmFtaWNfbWFuaWZlc3RzXzE1MDgyIjp7ImJ1Y2tldCI6InRydWVfYml0cmF0ZSIsInZlcnNpb24iOm51bGx9LCJ0ZndfbGVnYWN5X3RpbWVsaW5lX3N1bnNldCI6eyJidWNrZXQiOnRydWUsInZlcnNpb24iOm51bGx9LCJ0ZndfdHdlZXRfZWRpdF9mcm9udGVuZCI6eyJidWNrZXQiOiJvbiIsInZlcnNpb24iOm51bGx9fQ%3D%3D&frame=false&hideCard=false&hideThread=false&id=1808192447630942582&lang=en&origin=https%3A%2F%2Ffrankmbrown.net%2F&sessionId=a78d19810bc9ae1b8fd0e93ce8766b76c9ab481e&theme=light&widgetsVersion=2615f7e52b7e0%3A1702314776716&width=550px" style="position: static; visibility: visible; width: 550px; height: 753px; display: block; flex-grow: 1;" data-tweet-id="1808192447630942582"></iframe></div>
</div>
TikTok Videos
- Embedded TikTok videos initially look similar to below:
<div class="flex-row justify-center" style="min-height:750px;">
<blockquote class="tiktok-embed" cite="https://www.tiktok.com/@scout2015/video/6718335390845095173" data-video-id="6718335390845095173" data-embed-from="oembed" style="max-width:605px; min-width:325px;"> <section> <a target="_blank" title="@scout2015" href="https://www.tiktok.com/@scout2015?refer=embed">@scout2015</a> <p>Scramble up ur name & I’ll try to guess it😍❤️ <a title="foryoupage" target="_blank" href="https://www.tiktok.com/tag/foryoupage?refer=embed">#foryoupage</a> <a title="petsoftiktok" target="_blank" href="https://www.tiktok.com/tag/petsoftiktok?refer=embed">#petsoftiktok</a> <a title="aesthetic" target="_blank" href="https://www.tiktok.com/tag/aesthetic?refer=embed">#aesthetic</a></p> <a target="_blank" title="♬ original sound - tiff" href="https://www.tiktok.com/music/original-sound-6689804660171082501?refer=embed">♬ original sound - tiff</a> </section> </blockquote>
</div>
- After the
window.tiktokEmbed.lib.render(HTMLElement);
function is called, embedded TikTok videos look similar to below:
<div class="flex-row justify-center" style="min-height:750px;">
<blockquote class="tiktok-embed" cite="https://www.tiktok.com/@scout2015/video/6718335390845095173" data-video-id="6718335390845095173" data-embed-from="oembed" style="max-width:605px; min-width:325px;" id="v85528206007543570"> <iframe name="__tt_embed__v85528206007543570" sandbox="allow-popups allow-popups-to-escape-sandbox allow-scripts allow-top-navigation allow-same-origin" src="https://www.tiktok.com/embed/v2/6718335390845095173?lang=en-US&referrer=https%3A%2F%2Ffrankmbrown.net%2Fprojects%2Fembed-social-media-posts&embedFrom=oembed" style="width: 100%; height: 740px; display: block; visibility: unset;"></iframe></blockquote>
</div>
Instagram Posts
- Embedded Instagram Posts initially look similar to below:
<div class="flex-row justify-center" style="min-height: <%=locals.desktop?'730px':'540px'%>;">
<blockquote class="instagram-media" data-instgrm-captioned data-instgrm-permalink="https://www.instagram.com/p/C4gH3z_Jg1D/?utm_source=ig_embed&utm_campaign=loading" data-instgrm-version="14" style=" background:#FFF; border:0; border-radius:3px; box-shadow:0 0 1px 0 rgba(0,0,0,0.5),0 1px 10px 0 rgba(0,0,0,0.15); margin: 1px; max-width:540px; min-width:326px; padding:0; width:99.375%; width:-webkit-calc(100% - 2px); width:calc(100% - 2px);"><div style="padding:16px;"> <a href="https://www.instagram.com/p/C4gH3z_Jg1D/?utm_source=ig_embed&utm_campaign=loading" style=" background:#FFFFFF; line-height:0; padding:0 0; text-align:center; text-decoration:none; width:100%;" target="_blank"> <div style=" display: flex; flex-direction: row; align-items: center;"> <div style="background-color: #F4F4F4; border-radius: 50%; flex-grow: 0; height: 40px; margin-right: 14px; width: 40px;"></div> <div style="display: flex; flex-direction: column; flex-grow: 1; justify-content: center;"> <div style=" background-color: #F4F4F4; border-radius: 4px; flex-grow: 0; height: 14px; margin-bottom: 6px; width: 100px;"></div> <div style=" background-color: #F4F4F4; border-radius: 4px; flex-grow: 0; height: 14px; width: 60px;"></div></div></div><div style="padding: 19% 0;"></div> <div style="display:block; height:50px; margin:0 auto 12px; width:50px;"><svg width="50px" height="50px" viewBox="0 0 60 60" version="1.1" xmlns="https://www.w3.org/2000/svg" xmlns:xlink="https://www.w3.org/1999/xlink"><g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"><g transform="translate(-511.000000, -20.000000)" fill="#000000"><g><path d="M556.869,30.41 C554.814,30.41 553.148,32.076 553.148,34.131 C553.148,36.186 554.814,37.852 556.869,37.852 C558.924,37.852 560.59,36.186 560.59,34.131 C560.59,32.076 558.924,30.41 556.869,30.41 M541,60.657 C535.114,60.657 530.342,55.887 530.342,50 C530.342,44.114 535.114,39.342 541,39.342 C546.887,39.342 551.658,44.114 551.658,50 C551.658,55.887 546.887,60.657 541,60.657 M541,33.886 C532.1,33.886 524.886,41.1 524.886,50 C524.886,58.899 532.1,66.113 541,66.113 C549.9,66.113 557.115,58.899 557.115,50 C557.115,41.1 549.9,33.886 541,33.886 M565.378,62.101 C565.244,65.022 564.756,66.606 564.346,67.663 C563.803,69.06 563.154,70.057 562.106,71.106 C561.058,72.155 560.06,72.803 558.662,73.347 C557.607,73.757 556.021,74.244 553.102,74.378 C549.944,74.521 548.997,74.552 541,74.552 C533.003,74.552 532.056,74.521 528.898,74.378 C525.979,74.244 524.393,73.757 523.338,73.347 C521.94,72.803 520.942,72.155 519.894,71.106 C518.846,70.057 518.197,69.06 517.654,67.663 C517.244,66.606 516.755,65.022 516.623,62.101 C516.479,58.943 516.448,57.996 516.448,50 C516.448,42.003 516.479,41.056 516.623,37.899 C516.755,34.978 517.244,33.391 517.654,32.338 C518.197,30.938 518.846,29.942 519.894,28.894 C520.942,27.846 521.94,27.196 523.338,26.654 C524.393,26.244 525.979,25.756 528.898,25.623 C532.057,25.479 533.004,25.448 541,25.448 C548.997,25.448 549.943,25.479 553.102,25.623 C556.021,25.756 557.607,26.244 558.662,26.654 C560.06,27.196 561.058,27.846 562.106,28.894 C563.154,29.942 563.803,30.938 564.346,32.338 C564.756,33.391 565.244,34.978 565.378,37.899 C565.522,41.056 565.552,42.003 565.552,50 C565.552,57.996 565.522,58.943 565.378,62.101 M570.82,37.631 C570.674,34.438 570.167,32.258 569.425,30.349 C568.659,28.377 567.633,26.702 565.965,25.035 C564.297,23.368 562.623,22.342 560.652,21.575 C558.743,20.834 556.562,20.326 553.369,20.18 C550.169,20.033 549.148,20 541,20 C532.853,20 531.831,20.033 528.631,20.18 C525.438,20.326 523.257,20.834 521.349,21.575 C519.376,22.342 517.703,23.368 516.035,25.035 C514.368,26.702 513.342,28.377 512.574,30.349 C511.834,32.258 511.326,34.438 511.181,37.631 C511.035,40.831 511,41.851 511,50 C511,58.147 511.035,59.17 511.181,62.369 C511.326,65.562 511.834,67.743 512.574,69.651 C513.342,71.625 514.368,73.296 516.035,74.965 C517.703,76.634 519.376,77.658 521.349,78.425 C523.257,79.167 525.438,79.673 528.631,79.82 C531.831,79.965 532.853,80.001 541,80.001 C549.148,80.001 550.169,79.965 553.369,79.82 C556.562,79.673 558.743,79.167 560.652,78.425 C562.623,77.658 564.297,76.634 565.965,74.965 C567.633,73.296 568.659,71.625 569.425,69.651 C570.167,67.743 570.674,65.562 570.82,62.369 C570.966,59.17 571,58.147 571,50 C571,41.851 570.966,40.831 570.82,37.631"></path></g></g></g></svg></div><div style="padding-top: 8px;"> <div style=" color:#3897f0; font-family:Arial,sans-serif; font-size:14px; font-style:normal; font-weight:550; line-height:18px;">View this post on Instagram</div></div><div style="padding: 12.5% 0;"></div> <div style="display: flex; flex-direction: row; margin-bottom: 14px; align-items: center;"><div> <div style="background-color: #F4F4F4; border-radius: 50%; height: 12.5px; width: 12.5px; transform: translateX(0px) translateY(7px);"></div> <div style="background-color: #F4F4F4; height: 12.5px; transform: rotate(-45deg) translateX(3px) translateY(1px); width: 12.5px; flex-grow: 0; margin-right: 14px; margin-left: 2px;"></div> <div style="background-color: #F4F4F4; border-radius: 50%; height: 12.5px; width: 12.5px; transform: translateX(9px) translateY(-18px);"></div></div><div style="margin-left: 8px;"> <div style=" background-color: #F4F4F4; border-radius: 50%; flex-grow: 0; height: 20px; width: 20px;"></div> <div style=" width: 0; height: 0; border-top: 2px solid transparent; border-left: 6px solid #f4f4f4; border-bottom: 2px solid transparent; transform: translateX(16px) translateY(-4px) rotate(30deg)"></div></div><div style="margin-left: auto;"> <div style=" width: 0px; border-top: 8px solid #F4F4F4; border-right: 8px solid transparent; transform: translateY(16px);"></div> <div style=" background-color: #F4F4F4; flex-grow: 0; height: 12px; width: 16px; transform: translateY(-4px);"></div> <div style=" width: 0; height: 0; border-top: 8px solid #F4F4F4; border-left: 8px solid transparent; transform: translateY(-4px) translateX(8px);"></div></div></div> <div style="display: flex; flex-direction: column; flex-grow: 1; justify-content: center; margin-bottom: 24px;"> <div style=" background-color: #F4F4F4; border-radius: 4px; flex-grow: 0; height: 14px; margin-bottom: 6px; width: 224px;"></div> <div style=" background-color: #F4F4F4; border-radius: 4px; flex-grow: 0; height: 14px; width: 144px;"></div></div></a><p style=" color:#c9c8cd; font-family:Arial,sans-serif; font-size:14px; line-height:17px; margin-bottom:0; margin-top:8px; overflow:hidden; padding:8px 0 7px; text-align:center; text-overflow:ellipsis; white-space:nowrap;"><a href="https://www.instagram.com/p/C4gH3z_Jg1D/?utm_source=ig_embed&utm_campaign=loading" style=" color:#c9c8cd; font-family:Arial,sans-serif; font-size:14px; font-style:normal; font-weight:normal; line-height:17px; text-decoration:none;" target="_blank">A post shared by @americanrivieraorchard</a></p></div></blockquote>
</div>
- After the
window.instgrm.Embeds.process();
function is called, embedded Instagram posts look similar to below:
<div class="flex-row justify-center" style="min-height: 730px;">
<iframe class="instagram-media instagram-media-rendered" id="instagram-embed-0" src="https://www.instagram.com/p/C4gH3z_Jg1D/embed/captioned/?cr=1&v=14&wp=675&rd=https%3A%2F%2Ffrankmbrown.net&rp=%2Fprojects%2Fembed-social-media-posts#%7B%22ci%22%3A0%2C%22os%22%3A1118753.5%2C%22ls%22%3A117.30000001192093%2C%22le%22%3A117.30000001192093%7D" allowtransparency="true" allowfullscreen="true" frameborder="0" height="722" data-instgrm-payload-id="instagram-media-payload-0" scrolling="no" style="background: white; max-width: 540px; width: calc(100% - 2px); border-radius: 3px; border: 1px solid rgb(219, 219, 219); box-shadow: none; display: block; margin: 0px 0px 12px; min-width: 326px; padding: 0px;"></iframe>
</div>
Reddit Comments
- Embedded Reddit comments initially look similar to below:
<div class="flex-row justify-center" style="min-height: 404px;">
<blockquote class="reddit-embed-bq" data-embed-height="404"><a href="https://www.reddit.com/r/france/comments/1duaao0/comment/lbf52g6/">Comment</a><br> by<a href="https://www.reddit.com/user/Thor1noak/">u/Thor1noak</a> from discussion<a href="https://www.reddit.com/r/france/comments/1duaao0/ipsos_sociologie_des_électorats_législatives_2024/"><no value=""></no></a><br> in<a href="https://www.reddit.com/r/france/">france</a></blockquote>
</div>
- After the Reddit embed script is executed, embedded Reddit comments look similar to below:
<div class="flex-row justify-center" style="min-height: 404px;">
<iframe height="404" src="https://embed.reddit.com/r/france/comments/1duaao0/comment/lbf52g6/?embed=true&ref_source=embed&ref=share&utm_medium=widgets&utm_source=embedv2&utm_term=23&showmedia=false&showmore=false&depth=1&utm_name=comment_embed&embed_host_url=https%3A%2F%2Ffrankmbrown.net%2Fprojects%2Fembed-social-media-posts" width="640px" scrolling="no" allowfullscreen="true" sandbox="allow-scripts allow-same-origin allow-popups" allow="clipboard-read; clipboard-write" style="border: none; max-width: 100%; border-radius: 8px; display: block; margin: 0px auto;"></iframe>
</div>
Final Code
The MutatuonObserver turned out to not be the best option for removing the min-height from the wrapper <div>. Instead, you can check with javascript and setTimeout or setInterval for when the <iframe> is rendered and then remove the min-height from the wrapper <div>.
Comments
There are currently no comments to show for this article.