syncAttrsToElement
Writes an attribute object onto a real DOM element and removes what its previous call wrote, leaving attributes from other code alone.
import { syncAttrsToElement } from '@salla.sa/twilight-theme-engine/utils';In plain words
Adding classes to an element is easy. Removing exactly the ones you added earlier, without removing anyone else's, is not. syncAttrsToElement(element, attrs) remembers what it wrote in a data-de-managed attribute on the element; on the next call it removes those first, then writes the new set.
This is how the engine keeps the classes on <body> and <html> in step with the current page. It needs a real element, so call it in the browser, inside an effect.
Signature
function syncAttrsToElement(element: Element, next: Record<string, string>): void // next uses HTML names (class, not className), e.g. the output of mergeElementAttrs
Try it live
import { useEffect, useRef } from 'react';
import { mergeElementAttrs, syncAttrsToElement } from '@salla.sa/twilight-theme-engine/utils';
export function Promo({ children }: { children: React.ReactNode }) {
const box = useRef<HTMLDivElement>(null);
useEffect(() => {
if (!box.current) return;
// HTML attribute names (class, not className), merged first.
syncAttrsToElement(
box.current,
mergeElementAttrs([{ class: 'promo is-open', 'data-state': 'open' }])
);
}, []);
return <div ref={box}>{children}</div>;
}
Example
import { useEffect } from 'react';
import { mergeElementAttrs, syncAttrsToElement } from '@salla.sa/twilight-theme-engine/utils';
/** Marks a third-party chat widget's container while the cart drawer is open. */
export function useChatWidgetState(drawerOpen: boolean) {
useEffect(() => {
const widget = document.getElementById('chat-widget');
if (!widget) return;
syncAttrsToElement(
widget,
mergeElementAttrs([{ class: drawerOpen ? 'is-hidden' : '', 'data-drawer': drawerOpen ? 'open' : '' }])
);
return () => syncAttrsToElement(widget, {});
}, [drawerOpen]);
}
How it behaves
In order: read the previous record from
data-de-managed(malformed JSON is ignored), remove each class and attribute it lists, add the new classes withclassList.add, set the other attributes withsetAttribute, then storenextas the record, or remove the record whennextis empty.syncAttrsToElement(element, {})removes everything the last call wrote.Classes are added and removed one by one through
classList, so classes nobody managed stay. Other attributes are set and removed whole.Used by
DocumentClassProvider, byuseDocumentClass()when there is no provider, and byattrs/clearAttrsfrom@salla.sa/twilight-theme-engine/dom, always ondocument.bodyanddocument.documentElement.
Gotchas
Ownership is by name, not by writer: a class that other code also added is removed when it leaves the managed set. One element has one
data-de-managedrecord, so two callers syncing the same element erase each other's attributes.TwilightProvideralready syncs<body>and<html>; useuseDocumentClass()there, not this.It expects HTML names:
{ className: 'x' }sets an attribute namedclassname, not a class.Attributes written this way are not in the server HTML; they appear after hydration.
Related
The three steps of every html and body attribute merge: split class lists, merge attribute objects in order, rename class to className.
useDocumentClassAdds classes and attributes to the page <html> and <body> while the calling component is mounted, merged with everyone else.
DocumentClassProviderCollects every useDocumentClass() request in its tree and applies the merged classes and attributes to the real body and html elements.