@ng-native/web-compat
v0.19.2
Published
Opt-in DOM compatibility for Angular Native, so component libraries written for the browser can render as native views.
Maintainers
Readme
@ng-native/web-compat
Opt-in DOM compatibility for Angular Native. A component library written for the browser reaches
for the DOM on the elements it renders: getAttribute, classList, addEventListener. This
package gives the engine's nodes those members, each going through the engine's own mutation API,
so such a library can render as native views.
Experimental and partial. It has:
- Node names and kinds, namespaces and prefixes, attributes (in a browser's case, as
Attrnodes and in a namespace) and classes,dataset,textContent,innerText, and listeners withdispatchEvent. - Markup:
innerHTML,outerHTML,insertAdjacentHTML,setHTMLand its kin, and a<template>'scontent, parsed as a browser parses HTML. - The properties HTML reflects attributes as (
hidden,title,dir,tabIndex,draggable,type, ARIA's and the rest),isContentEditablefromcontenteditable, a table's rows and cells, and the element interfaces,HTMLInputElementand the rest, forinstanceof. - The tree: live
childNodesandchildren, the nodes around one,appendChild,insertBefore,removeChild,replaceChild,append,prepend,replaceChildren,before,after,replaceWith,insertAdjacentElement,moveBefore,cloneNode,getRootNode,compareDocumentPosition,isEqualNodeandnormalize, each checked as a browser checks an insertion. Texts and comments havedataand the rest ofCharacterData. - Live
getElementsByTagName,getElementsByClassNameandgetElementsByName. - Selectors:
querySelectorandquerySelectorAllon an element, a fragment and a document,matches,closest, read as Selectors Level 4 has them, with aSyntaxErrorfor one that cannot be read. A state nothing here reads,:hoveror:checked, matches nothing. - Stylesheets:
document.styleSheets, a<style>'ssheet,CSSStyleSheet(made by a script too, withreplace,replaceSyncandadoptedStyleSheets), its rules withinsertRuleanddeleteRule, and the rest of the CSSOM's rule classes. A<style>a library makes while the app runs is read and applied, for the layout properties, colours, custom properties and transitions it declares,@mediaby the window's width and height, which way up it is and the colour scheme and motion the user asked for, and@supports. element.styleas aCSSStyleDeclaration:cssText,setProperty,getPropertyValue,removePropertyand each property by name, shorthands as their longhands, and thestyleattribute as its text. A style a library binds is set as what a native view reads.getComputedStyle, with awidthorheightan element has none of its own of as it is drawn, ahiddenelement drawn asdisplay: none, andCSS.escapeandCSS.supports.- Geometry:
getBoundingClientRectandgetClientRectsasDOMRects,offsetParentand theoffset,clientandscrollsizes and positions, each read from where native last laid the element out with what is pending committed first.document.elementFromPointandelementsFromPointanswer the element drawn at a point, passing overpointer-events: none,display: noneand what a box that clips its overflow does not draw. - Scrolling: an element whose style says
overflow: autoorscrollscrolls what overflows it, withscrollTopandscrollLeftto read and write,scroll,scrollTo,scrollByandscrollIntoView. The window does not scroll, anddocument.scrollingElementis its root. - The window:
screen,visualViewport,matchMediawith achangelistener, andResizeObserverandIntersectionObserver, with a root, a margin and thresholds. An app that observes nothing pays nothing for them. navigator.userAgentnaming a phone of the platform the app runs on, andmaxTouchPoints, where the runtime's ownnavigatorsays no browser: a library picks touch handling from them.- Stand-ins for what a library asks of the window:
localStorageandsessionStorageheld in memory for as long as the app is up,getSelection()with nothing selected,open()returningnull, and aShadowRootthat nothing is an instance of. - The window starts under the navigation header of the screen in sight and ends at the top of the
keyboard while it is up, as a browser on a phone ends its window there:
innerHeight,visualViewportand where an element or a touch is are read from there,resizeis sent as either moves, anddocument.body, where an overlay goes, covers just that part of the screen, with what isfixedin it fixed to it. On Android, which draws an app under its system bars, the window is as tall as the app is drawn.screenandmatchMediaread the whole screen. - On Android, where
react-native-screensmeasures a view in a screen from under its navigation header, an element in a screen is placed where it is drawn and touched, so a library that compares a finger with what it measured finds the element under the finger. A touch says whether native measures short, and until one does an element in a screen is taken to be. - Animations:
getAnimations(), on an element and withsubtreeon what it holds, gives each@keyframesanimation and transition the engine runs there as a Web AnimationsAnimation, withplayState, afinishedthat settles when it ends or stops,finish(),cancel(), and aneffectwithgetTiming(),getComputedTiming()andupdateTiming(), whosefill: 'forwards'holds an@keyframesanimation at its last frame. MutationObserver, withchildList,attributes,characterData,subtreeand old values.- A
documentthat creates engine nodes and fragments, with abodythat draws over the screen for overlays (what a library puts in it is first seen in the frame after, by when it has been measured and styled, as a browser first paints it, and stays a view native does not flatten away), a doctype,createElementNS,createTreeWalkerandcreateNodeIterator, and adocument.implementationthat makes HTML and XML documents that are never drawn. - Events:
MouseEvent,PointerEvent,KeyboardEvent,FocusEvent,TouchEventand the other kinds,document.createEventwith each kind'sinit*Event,window.event, and handler properties,onclicktoonload. A press is aPointerEvent, and a touch is the mouse going down and up as a phone's browser has it. A held finger is acontextmenu. An element moved to another parent, or taken out, while a finger is on it is moved in the document at once and stays where native has it until the finger lifts, so the touch goes on, as a drag that puts its element on the body as it begins needs. - What a click does: a checkbox or a radio is checked and says
inputandchange, a button submits or resets its form, and a label clicks or focuses its control, for a press, a click a script sends andelement.click(). focus()andblur()as a browser has them: at once, on a button, a link or what has atabindexas on a text field, withdocument.activeElement,:focusandfocusinandfocusouton the document.- The basic
windowglobals: its listeners,Event,CustomEvent,HTMLElement,Node,Text,Comment,DocumentFragment,NodeList,HTMLCollection,DOMParser,XMLSerializer,Document,Attr,CSS,MediaList, the window's size, and an element with an id as a global by it. - SVG: an
<svg>a template writes, markup builds or a script makes withcreateElementNSis drawn by react-native-svg's native views, the ones@ng-native/iconsdraws with: shapes, groups, texts and their spans, a text along a path, gradients and their stops, clips, masks, markers, patterns, a drop shadow and HTML in aforeignObject, the whole drawing where it gives no area. Its elements have SVG's names and namespace,classNameas an object withbaseVal,getBBox,getComputedTextLength,getNumberOfChars,getSubStringLength,ownerSVGElementandcreateSVGRect, and a paint that names a token is the token's colour. A shape with a listener for a touch, or in a group or a drawing with one, takes the touch; a touch native finds no shape for, as between the letters of a text, is on the text, rectangle, circle or ellipse whose box is under the finger; and a touch on a shape sendsmouseoverandmouseenter, andmouseoutandmouseleavefor the shape before, as a phone's browser does, so a chart shows a series' tooltip.@ng-native/iconsis a peer dependency for this. The app installsreact-native-svgfor the native views; none of its JavaScript is imported, and without it a drawing shows nothing, as an icon does. A<path>of thousands of straight segments that go one way across, a chart's line series, is drawn thinned to the lowest and highest point of each point-wide column, as react-native-svg on iOS takes seconds over a stroke that long;getAttribute('d')andgetBBoxread the whole path, and a canvas's long line is thinned too. A curve, a relative command, a path that turns back so often that no run is long, or one under a rotation is drawn as written. - Canvas: a
<canvas>'s 2D context draws with the same native views, so it too shows nothing where the app has not installedreact-native-svg. Itswidthandheightare the grid it draws on, and the shapes drawn, not its text, read back bygetImageData, draw on another canvas bydrawImageand are written as a PNG bytoDataURLandtoBlob. On React Native,toBlobhands its callback null, as itsBlobholds no bytes;toDataURLhas the PNG there. There is no bitmap: a clear of part of it, and a way of laying a drawing over what is there other thansource-over(andcopywhere nothing clips it), change only the pixels read back. An<img>draws on one once its picture has loaded, and is not in the pixels read back. <img>as the platform's image view, from itssrcresolved against the page's address, with itsaltsaid to a screen reader, itswidthandheightas its size, the size of its picture where nothing else sizes it,naturalWidth,naturalHeight,complete,loadanderror, andobject-fitas how the picture fits its box. A picture that is an SVG is fetched and drawn as an<svg>of its shapes alone. An image a library's sheet or a script clips to a band of itself,clip-path: polygon(a 0, b 0, b 100%, a 100%), is drawn through the band, as a before-and-after slider cuts one picture over another. In a library's sheet an edge can be a token,var(--position, 50%), and the band follows the token as a script sets it.- An HTML element starts as a browser's own stylesheet has it:
content-box, a block where HTML has one,position: static(an absolute element is placed against the nearest one that saysrelative), and as a flex itemflex-shrink: 1down to its content. A button has its label in the middle of its height until a stylesheet lays it out. - A
<button>and an<a>with their roles, each taking a press with no listener (:activewhile held) and markeddata-hoverwhile a pointer is over it, a disabled one announced as disabled, and a table's and a form's elements as views. - A table laid out in columns: a cell as wide as the columns it is across (
colspan), one down several rows (rowspan), the columns sharing the table's width by what their cells hold or the widths the cells say, andtable-layout: fixedgiving them one width. - The page's address:
document.URL,baseURIand<base href>,window.location, a link'shrefand the parts of its URL, the other URLs an element's attributes say, resolved against it, and:target. <iframe>, as a native web view or as a document of its own, withcontentWindow,postMessageboth ways, andwindow.frames.- A
clicklistener that hears a press. - Forms: an
<input>and a<textarea>are the platform's text field, withvalueanddefaultValue, its keys,changeonce an edit ends, the keyboard itstypeandinputmodeask for, twenty characters wide until itssizesays otherwise, acaret-colorin itsstyleas its selection colour (transparenthides the caret), andreadOnlyanddisabledmaking it one that cannot be typed into. An<input type="range">keeps its value within itsminandmaxon itsstep, hasvalueAsNumber,stepUp()andstepDown(), and moves under a finger dragged across it, sayinginputas it moves andchangeas the finger lifts. A<select>shows its chosen<option>and opens a list of them when it or its label is pressed. A control haschecked,validitywithcheckValidity(),reportValidity()andsetCustomValidity(),selectionStartandsetSelectionRange(),select()followed bydocument.execCommand('copy')writing the text to the clipboard throughexpo-clipboard, and itsform, which haselements,requestSubmit()andreset(). A disabled<fieldset>disables what is in it, its<legend>sits on its top border, a file input'sclick()opens the system's file picker throughexpo-document-picker, and:valid,:checked,:placeholder-shownand the rest match. An app opens a picker of its own instead by providingFILE_PICKER, an object with thegetDocumentAsync()ofexpo-document-picker(its type isFilePicker).
The rest of the DOM is not part of it yet.
Nothing in an app changes until it adds the provider, and everything the provider adds is taken away again when the app is destroyed, but for one thing: an element of that app still answers as one, as an element out of the document does in a browser, so a library's timer that fires after its app is gone (a ripple that fades, a class a field adds) has something to call. An element of an app that never asked has none of it.
Install
npm install @ng-native/web-compatUse
import { provideWebCompat } from '@ng-native/web-compat';
export const appConfig = {
providers: [provideWebCompat()],
};It gives Angular's Location a platform to stand on in an app with no router. In an app with the
native router, Location is the router's, in whichever order the two are listed.
Name what is written for a browser
A library's stylesheets are read as a browser reads them only for the packages the build is given: a flex container is a row. Wrap the Metro config last:
const { withWebCompat } = require('@ng-native/web-compat/metro');
module.exports = withWebCompat(config, {
libraries: ['@angular/cdk', '@angular/material'],
// Components a library's CLI copies into the app, as Spartan UI's styled layer is.
sources: ['./src/app/ui'],
});libraries are npm packages. sources are directories of the app's own sources, relative to the
project. Every other component in the app stays the native view it is.
withWebCompat also gives the window and the document their addEventListener,
removeEventListener and dispatchEvent before any module runs, for a library that adds a
listener as it loads: the CDK does so on the window to learn whether one can be passive, and a
library noting the last touch does so on the document. What the window is given is handed to the
window once an app is up, and what the document is given to each app's document, so such a library
can be imported anywhere in the app, not only on a lazy route.
A library's icons
A library that draws its icons with NgIcon from @ng-icons/core, as Spartan UI's helm does,
sets each icon's SVG markup as innerHTML, which no native view draws. withWebCompat resolves
@ng-icons/core to @ng-native/web-compat/ng-icons: the same module, with NgIcon the one from
@ng-native/icons, which takes the same inputs and draws the same icons as native shapes. It is
resolved so for the library and for the app, and webCompat() does the same in Vitest. Wrap the
Metro config with withWebCompat last, so a resolver another helper set is the one it goes
through.
A library that asks whether it is in a browser
Angular's PLATFORM_ID is native on a device, and some libraries open an overlay or measure
only where it is browser. Give the component or the route of such a library's page a browser's
id. The rest of the app keeps reading native.
import { provideBrowserPlatformId } from '@ng-native/web-compat';
@Component({
providers: [provideBrowserPlatformId()],
})
export class SettingsPage {}The page's address
The page is at the app's own URL, with the route the app is on: document.URL,
document.baseURI and window.location say it, and a relative link, an image's src and the
other URLs an element's attributes say resolve against it, as a browser resolves them against
the address of the page it shows. A <base href> in the head changes what they resolve against.
The app's URL is the one Expo opens the app with: exp://127.0.0.1:8081/--/ in Expo Go, and the
app config's scheme in a build, so canary:// is a page at canary:///settings. An app with
neither is at app://. Give it one that wins over both:
provideWebCompat({ url: 'https://shop.example/' });location.assign(), replace() and setting href or a part of it go to a page of the app the
way a link to it does, through the router, and open any other URL with Linking. In an app
without the native router, a page of the app goes nowhere. reload() does nothing.
Frames
An <iframe> is a native web view, from react-native-webview, which shows the page in it as a
browser does, its scripts included. Install it in an app that has a frame:
npx expo install react-native-webviewsrc resolves against the page's address, and srcdoc is shown with the page's address as its
base where that is an http, https or file address and no sandbox takes the frame's origin
away. A frame is 300 by 150 points, as in a browser, under its
width and height and any stylesheet, and hears load as its page finishes loading.
The app has the frame as a browser has a frame of another origin: contentDocument is null,
and the two talk with postMessage. iframe.contentWindow.postMessage(data, origin) is a
message event in the page, from the app's origin, whose source the page answers through. The
page's window.parent.postMessage(data, origin), and window.ReactNativeWebView.postMessage,
are a message event on the app's window, whose source is the frame's contentWindow.
The postMessage methods cross as JSON; what ReactNativeWebView.postMessage sends arrives as
the string it is.
The page is sent as the frame's Referer, as referrerpolicy says, where its address is a web
address: a video embed that asks who embeds it needs provideWebCompat({ url }) to be one in
Expo Go, whose exp:// address is not. sandbox without allow-scripts runs no script, and
allow turns on autoplay, fullscreen and picture in picture where it lists them. loading and
the rest of sandbox are left out, and development says so.
sandbox without allow-same-origin gives the frame the origin null in the messages the app
sends and hears, and shows srcdoc with no base. A native web view cannot give a page no origin,
so a src page in such a sandbox still loads at its own origin, with its storage and cookies,
and development says so. The sandbox says who the app talks to; it does not isolate the page.
Where react-native-webview is not installed, and in Node, a frame is an emulated document
instead: a document of the app's own, from about:blank, srcdoc or what src names, which a
script reads and writes as one of its own origin. It is not drawn and no script in it runs. Ask
for it in an app that measures or prints in a frame, and in a test that reads a frame's document:
provideWebCompat({ frames: 'emulated' });loadFrame reads what an emulated frame's src names, with fetch where it is not given.
