mount-observer 0.0.111 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Events.js +28 -26
- package/Events.ts +34 -30
- package/MountObserver.js +235 -520
- package/MountObserver.ts +281 -542
- package/README.md +149 -56
- package/attrCoordinates.js +93 -0
- package/attrCoordinates.ts +122 -0
- package/constants.js +6 -0
- package/constants.ts +7 -0
- package/index.js +3 -0
- package/index.ts +19 -0
- package/loadImports.js +47 -0
- package/loadImports.ts +56 -0
- package/package.json +8 -115
- package/playwright.config.ts +0 -1
- package/types.d.ts +86 -0
- package/whereAttr.js +174 -0
- package/whereAttr.ts +221 -0
- package/LICENSE +0 -21
- package/Newish.js +0 -145
- package/Newish.ts +0 -169
- package/ObsAttr.js +0 -18
- package/ObsAttr.ts +0 -18
- package/RootMutObs.js +0 -49
- package/RootMutObs.ts +0 -58
- package/Synthesizer.js +0 -125
- package/Synthesizer.ts +0 -130
- package/bindish.js +0 -15
- package/bindish.ts +0 -22
- package/compose.js +0 -148
- package/compose.ts +0 -164
- package/doCleanup.js +0 -31
- package/doCleanup.ts +0 -34
- package/getWhereAttrSelector.js +0 -83
- package/getWhereAttrSelector.ts +0 -92
- package/preloadContent.js +0 -44
- package/preloadContent.ts +0 -47
- package/readAttrs.ts +0 -60
- package/refid/README.md +0 -259
- package/refid/arr.js +0 -4
- package/refid/arr.ts +0 -4
- package/refid/camelToKebab.js +0 -4
- package/refid/camelToKebab.ts +0 -4
- package/refid/genIds.js +0 -190
- package/refid/genIds.ts +0 -177
- package/refid/getAdjRefs.js +0 -38
- package/refid/getAdjRefs.ts +0 -38
- package/refid/getContext.js +0 -13
- package/refid/getContext.ts +0 -14
- package/refid/getCount.js +0 -8
- package/refid/getCount.ts +0 -8
- package/refid/getIsh.js +0 -35
- package/refid/getIsh.ts +0 -37
- package/refid/hostish.js +0 -18
- package/refid/hostish.ts +0 -20
- package/refid/ism.js +0 -78
- package/refid/ism.ts +0 -81
- package/refid/itemprops.js +0 -60
- package/refid/itemprops.ts +0 -67
- package/refid/joinMatching.js +0 -56
- package/refid/joinMatching.ts +0 -54
- package/refid/nudge.js +0 -23
- package/refid/nudge.ts +0 -23
- package/refid/regIsh.js +0 -27
- package/refid/regIsh.ts +0 -31
- package/refid/secretKeys.js +0 -5
- package/refid/secretKeys.ts +0 -5
- package/refid/splitRefs.js +0 -6
- package/refid/splitRefs.ts +0 -6
- package/refid/stdVal.js +0 -15
- package/refid/stdVal.ts +0 -15
- package/refid/via.js +0 -114
- package/refid/via.ts +0 -113
- package/slotkin/affine.js +0 -39
- package/slotkin/affine.ts +0 -46
- package/slotkin/beKindred.js +0 -45
- package/slotkin/beKindred.ts +0 -55
- package/slotkin/getBreadth.js +0 -19
- package/slotkin/getBreadth.ts +0 -21
- package/slotkin/getFrag.js +0 -22
- package/slotkin/getFrag.ts +0 -21
- package/slotkin/toQuery.js +0 -12
- package/slotkin/toQuery.ts +0 -13
- package/slotkin/wrap.js +0 -13
- package/slotkin/wrap.ts +0 -18
- package/ts-refs/LICENSE +0 -21
- package/ts-refs/README.md +0 -18
- package/ts-refs/be-a-beacon/types.d.ts +0 -22
- package/ts-refs/be-alit/types.d.ts +0 -1
- package/ts-refs/be-based/types.d.ts +0 -32
- package/ts-refs/be-bound/types.d.ts +0 -65
- package/ts-refs/be-buttoned-up/types.d.ts +0 -21
- package/ts-refs/be-calculating/types.d.ts +0 -57
- package/ts-refs/be-clonable/types.d.ts +0 -28
- package/ts-refs/be-committed/types.d.ts +0 -26
- package/ts-refs/be-consoling/types.d.ts +0 -25
- package/ts-refs/be-counted/types.d.ts +0 -88
- package/ts-refs/be-delible/types.d.ts +0 -26
- package/ts-refs/be-directive/types.d.ts +0 -43
- package/ts-refs/be-dispatching/types.d.ts +0 -41
- package/ts-refs/be-elevating/types.d.ts +0 -55
- package/ts-refs/be-enhanced/types.d.ts +0 -32
- package/ts-refs/be-enhancing/types.d.ts +0 -31
- package/ts-refs/be-evanescent/types.d.ts +0 -20
- package/ts-refs/be-eventing/types.d.ts +0 -27
- package/ts-refs/be-exportable/types.d.ts +0 -26
- package/ts-refs/be-fetching/types.d.ts +0 -73
- package/ts-refs/be-flashy/types.d.ts +0 -27
- package/ts-refs/be-formalizing/types.d.ts +0 -29
- package/ts-refs/be-formidable/types.d.ts +0 -64
- package/ts-refs/be-giddy/types.d.ts +0 -26
- package/ts-refs/be-gingerly/types.d.ts +0 -19
- package/ts-refs/be-gone/types.d.ts +0 -24
- package/ts-refs/be-hashing-out/types.d.ts +0 -22
- package/ts-refs/be-hive/types.d.ts +0 -18
- package/ts-refs/be-imbued/types.d.ts +0 -30
- package/ts-refs/be-included/types.d.ts +0 -20
- package/ts-refs/be-inclusive/types.d.ts +0 -30
- package/ts-refs/be-intersectional/types.d.ts +0 -37
- package/ts-refs/be-intl/types.d.ts +0 -28
- package/ts-refs/be-invoking/types.d.ts +0 -28
- package/ts-refs/be-joining/types.d.ts +0 -26
- package/ts-refs/be-kvetching/types.d.ts +0 -24
- package/ts-refs/be-lazy/types.d.ts +0 -29
- package/ts-refs/be-literate/types.d.ts +0 -29
- package/ts-refs/be-mediating/types.d.ts +0 -34
- package/ts-refs/be-methodical/types.d.ts +0 -20
- package/ts-refs/be-modding/types.d.ts +0 -18
- package/ts-refs/be-observant/types.d.ts +0 -27
- package/ts-refs/be-observing/types.d.ts +0 -84
- package/ts-refs/be-parsed/types.d.ts +0 -19
- package/ts-refs/be-parsing/types.d.ts +0 -37
- package/ts-refs/be-persistent/types.d.ts +0 -66
- package/ts-refs/be-propagating/types.d.ts +0 -26
- package/ts-refs/be-reformable/types.d.ts +0 -48
- package/ts-refs/be-render-neutral/types.d.ts +0 -31
- package/ts-refs/be-scoped/types.d.ts +0 -24
- package/ts-refs/be-sharing/types.d.ts +0 -17
- package/ts-refs/be-switched/types.d.ts +0 -155
- package/ts-refs/be-typed/types.d.ts +0 -36
- package/ts-refs/be-value-added/types.d.ts +0 -34
- package/ts-refs/be-valued/types.d.ts +0 -22
- package/ts-refs/be-written/types.d.ts +0 -59
- package/ts-refs/css-charts/types.d.ts +0 -38
- package/ts-refs/css-echarts/types.d.ts +0 -13
- package/ts-refs/data-props/types.d.ts +0 -27
- package/ts-refs/do-inc/types.d.ts +0 -28
- package/ts-refs/do-invoke/types.d.ts +0 -28
- package/ts-refs/do-toggle/types.d.ts +0 -27
- package/ts-refs/em-bower/types.d.ts +0 -18
- package/ts-refs/fetch-for/types.d.ts +0 -37
- package/ts-refs/folder-picker/types.d.ts +0 -43
- package/ts-refs/for-fetch/doc.d.ts +0 -98
- package/ts-refs/for-fetch/types.d.ts +0 -83
- package/ts-refs/mount-observer/types.d.ts +0 -248
- package/ts-refs/mt-si/types.d.ts +0 -21
- package/ts-refs/per-each/types.d.ts +0 -51
- package/ts-refs/soak-up/types.d.ts +0 -36
- package/ts-refs/trans-render/XV/types.d.ts +0 -69
- package/ts-refs/trans-render/asmr/types.d.ts +0 -138
- package/ts-refs/trans-render/be/types.d.ts +0 -190
- package/ts-refs/trans-render/dss/types.d.ts +0 -57
- package/ts-refs/trans-render/froop/types.d.ts +0 -416
- package/ts-refs/trans-render/funions/types.d.ts +0 -12
- package/ts-refs/trans-render/lib/mixins/types.d.ts +0 -42
- package/ts-refs/trans-render/lib/prs/types.d.ts +0 -40
- package/ts-refs/trans-render/lib/types.d.ts +0 -489
- package/ts-refs/trans-render/types.d.ts +0 -583
- package/ts-refs/wc-info/SimpleWCInfo.d.ts +0 -15
- package/ts-refs/when-resolved/types.d.ts +0 -30
- package/ts-refs/xp-as/types.d.ts +0 -20
- package/ts-refs/xtal-element/types.d.ts +0 -43
- package/ts-refs/xtal-frappe-chart/types.d.ts +0 -193
- package/upShadowSearch.js +0 -25
- package/upShadowSearch.ts +0 -23
- package/waitForEvent.js +0 -12
- package/waitForEvent.ts +0 -13
- package/waitForIsh.js +0 -21
- package/waitForIsh.ts +0 -20
package/README.md
CHANGED
|
@@ -62,9 +62,7 @@ The amount of code necessary to accomplish these common tasks designed to improv
|
|
|
62
62
|
3. As discussed earlier, to do the job right, polyfills really need to reexamine **all** the elements within the observed node for matches **anytime any element within the Shadow Root so much as sneezes (has attribute modified, changes custom state, etc)**, due to modern selectors such as the :has selector. Surely, the platform has found ways to do this more efficiently?
|
|
63
63
|
|
|
64
64
|
The extra flexibility this new primitive would provide could be quite useful to things other than lazy loading of custom elements, such as implementing [custom enhancements](https://github.com/WICG/webcomponents/issues/1000) as well as [binding from a distance](https://github.com/WICG/webcomponents/issues/1035#issuecomment-1806393525) in userland.
|
|
65
|
-
|
|
66
|
-
> [!Note]
|
|
67
|
-
> Reading through the historical links tied to the selector-observer proposal this proposal helped spawn, I may have painted an overly optimistic picture of [what the platform is capable of](https://github.com/whatwg/dom/issues/398). It does leave me a little puzzled why this isn't an issue when it comes to styling, and also if some of the advances that were utilized to support :has could be applied to this problem space, so that maybe the arguments raised there have weakened. Even if the concerns raised are as relevant today, I think considering the use cases this proposal envisions, that the objections could be overcome, for the following reasons: 1. For scenarios where lazy loading is the primary objective, "bunching" multiple DOM mutations together and only reevaluating when things are quite idle is perfectly reasonable. Also, for binding from a distance, most of the mutations that need responding to quickly will be when the *state of the host* changes, so DOM mutations play a somewhat muted role in that regard. Again, bunching multiple DOM mutations together, even if adds a bit of a delay, also seems reasonable. I also think the platform could add an "analysis" step to look at the query and categorize it as "simple" queries vs complex. Selector queries that are driven by the characteristics of the element itself (localName, attributes, etc) could be handled in a more expedited fashion. Those that the platform does expect to require more babysitting could be monitored for less vigilantly. Maybe in the latter case, a console.warning could be emitted during initialization. The other use case, for lazy loading custom elements and custom enhancements based on attributes, I think most of the time this would fit the "simple" scenario, so again there wouldn't be much of an issue.
|
|
65
|
+
|
|
68
66
|
|
|
69
67
|
## First use case -- lazy loading custom elements
|
|
70
68
|
|
|
@@ -72,26 +70,26 @@ To specify the equivalent of what the alternative proposal linked to above would
|
|
|
72
70
|
|
|
73
71
|
```JavaScript
|
|
74
72
|
const observer = new MountObserver({
|
|
75
|
-
|
|
73
|
+
select:'my-element',
|
|
76
74
|
import: './my-element.js',
|
|
77
|
-
do: {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
disconnectedSignal: new AbortController().signal
|
|
86
|
-
});
|
|
75
|
+
do: ({localName}, {modules, observer, observeInfo}) => {
|
|
76
|
+
if(!customElements.get(localName)) {
|
|
77
|
+
customElements.define(localName, modules[0].MyElement);
|
|
78
|
+
}
|
|
79
|
+
observer.disconnectedSignal.abort();
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
}, {disconnectedSignal: new AbortController().signal});
|
|
87
83
|
observer.observe(document);
|
|
88
84
|
```
|
|
89
85
|
|
|
86
|
+
The do function will *only be called once per matching element* -- i.e. if the element stops matching the "select" criteria, then matches again, the do function won't be called again. It will be called for all elements when they match within the scope passed in to the observe method. However, the events discussed below, as well as more structured inline functions also as discussed below, will continue to be called repeatedly.
|
|
87
|
+
|
|
90
88
|
The constructor argument can also be an array of objects that fit the pattern shown above.
|
|
91
89
|
|
|
92
|
-
In fact, as we will see, where it makes sense, where we see examples that are strings, we will also allow for arrays of such strings. For example, the "
|
|
90
|
+
In fact, as we will see, where it makes sense, where we see examples that are strings, we will also allow for arrays of such strings. For example, the "select" key can point to an array of CSS selectors (and in this case the mount/dismount callbacks would need to provide an index of which one matched). I only recommend adding this complexity if what I suspect is true -- providing this support can reduce "context switching" between threads / memory spaces (c++ vs JavaScript), and thus improve performance. If multiple "on" selectors are provided, and multiple ones match, I think it makes sense to indicate the one with the highest specifier that matches. It would probably be helpful in this case to provide a special event that allows for knowing when the matching selector with the highest specificity changes for mounted elements.
|
|
93
91
|
|
|
94
|
-
If no imports are specified, it would go straight to do
|
|
92
|
+
If no imports are specified, it would go straight to do (if any such callbacks are specified), and it will also dispatch events as discussed below.
|
|
95
93
|
|
|
96
94
|
This only searches for elements matching 'my-element' outside any shadow DOM.
|
|
97
95
|
|
|
@@ -99,24 +97,52 @@ But the observe method can accept a node within the document, or a shadowRoot, o
|
|
|
99
97
|
|
|
100
98
|
The "observer" constant above is a class instance that inherits from EventTarget, which means it can be subscribed to by outside interests.
|
|
101
99
|
|
|
100
|
+
> [!Note]
|
|
101
|
+
> Reading through the historical links tied to the selector-observer proposal this proposal helped spawn, I may have painted an overly optimistic picture of [what the platform is capable of](https://github.com/whatwg/dom/issues/398). It does leave me a little puzzled why this isn't an issue when it comes to styling, and also if some of the advances that were utilized to support :has could be applied to this problem space, so that maybe the arguments raised there have weakened. Even if the concerns raised are as relevant today, I think considering the use cases this proposal envisions, that the objections could be overcome, for the following reasons: 1. For scenarios where lazy loading is the primary objective, "bunching" multiple DOM mutations together and only reevaluating when things are quite idle is perfectly reasonable. Also, for binding from a distance, most of the mutations that need responding to quickly will be when the *state of the host* changes, so DOM mutations play a somewhat muted role in that regard. Again, bunching multiple DOM mutations together, even if adds a bit of a delay, also seems reasonable. I also think the platform could add an "analysis" step to look at the query and categorize it as "simple" queries vs complex. Selector queries that are driven by the characteristics of the element itself (localName, attributes, etc) could be handled in a more expedited fashion. Those that the platform does expect to require more babysitting could be monitored for less vigilantly. Maybe in the latter case, a console.warning could be emitted during initialization. The other use case, for lazy loading custom elements and custom enhancements based on attributes, I think most of the time this would fit the "simple" scenario, so again there wouldn't be much of an issue.
|
|
102
|
+
|
|
103
|
+
In fact, I have encountered statements made by the browser vendors that some queries supported by css can't be evaluated simply by looking at the layout of the HTML, but has to be made after rendering and performing style calculations. This necessitates having to delay the notification, which would be unacceptable.
|
|
104
|
+
|
|
105
|
+
If the developer has a simple query in mind that needs no such nuance, I'm thinking it might be helpful to provide an alternative key to "on" that is used specifically for (a subset?) of queries supported by the existing "matches" method that elements support.
|
|
106
|
+
|
|
107
|
+
So the developer could use:
|
|
108
|
+
|
|
109
|
+
## Polyfill Supported Scenario I
|
|
110
|
+
|
|
111
|
+
```JavaScript
|
|
112
|
+
const observer = new MountObserver({
|
|
113
|
+
import: './my-element.js',
|
|
114
|
+
whereElementMatches:'my-element',
|
|
115
|
+
do: ({localName}, {modules, observer, observeInfo}) => {
|
|
116
|
+
if(!customElements.get(localName)) {
|
|
117
|
+
customElements.define(localName, modules[0].MyElement);
|
|
118
|
+
}
|
|
119
|
+
observer.disconnectedSignal.abort();
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
}, {disconnectedSignal: new AbortController().signal});
|
|
123
|
+
observer.observe(document);
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
and could perhaps expect faster binding as a result of the more limited supported expressions. Since "select" is not specified, it is assumed to be "*"
|
|
127
|
+
|
|
128
|
+
This polyfill in fact only supports this latter option ("whreElementMatches"), and leaves "select" for such a time as when a selector observer is available in the platform.
|
|
129
|
+
|
|
102
130
|
## The import key
|
|
103
131
|
|
|
104
132
|
This proposal has been amended to support multiple imports, including of different types:
|
|
105
133
|
|
|
106
134
|
```JavaScript
|
|
107
135
|
const observer = new MountObserver({
|
|
108
|
-
|
|
136
|
+
select:'my-element',
|
|
109
137
|
import: [
|
|
110
138
|
['./my-element-small.css', {type: 'css'}],
|
|
111
139
|
'./my-element.js',
|
|
112
140
|
],
|
|
113
|
-
do: {
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
customElements.define(localName, modules[1].MyElement);
|
|
117
|
-
}
|
|
118
|
-
observer.disconnectedSignal.abort();
|
|
141
|
+
do: ({localName}, {modules, observer}) => {
|
|
142
|
+
if(!customElements.get(localName)) {
|
|
143
|
+
customElements.define(localName, modules[1].MyElement);
|
|
119
144
|
}
|
|
145
|
+
observer.disconnectedSignal.abort();
|
|
120
146
|
}
|
|
121
147
|
});
|
|
122
148
|
observer.observe(document);
|
|
@@ -124,11 +150,11 @@ observer.observe(document);
|
|
|
124
150
|
|
|
125
151
|
Once again, the key can accept either a single import, but alternatively it can also support multiple imports (via an array).
|
|
126
152
|
|
|
127
|
-
The do
|
|
153
|
+
The do function won't be invoked until all the imports have been successfully completed and inserted into the modules array.
|
|
128
154
|
|
|
129
155
|
Previously, this proposal called for allowing arrow functions as well, thinking that could be a good interim way to support bundlers, as well as multiple imports. But the valuable input provided by [doeixd](https://github.com/doeixd) makes me think that that interim support could more effectively be done by the developer in the do methods.
|
|
130
156
|
|
|
131
|
-
This proposal would also include support for JSON and HTML module imports.
|
|
157
|
+
This proposal would also include support for JSON and HTML module imports (really, all types).
|
|
132
158
|
|
|
133
159
|
## Preemptive downloading
|
|
134
160
|
|
|
@@ -137,21 +163,19 @@ There are two significant steps to imports, each of which imposes a cost:
|
|
|
137
163
|
1. Downloading the resource.
|
|
138
164
|
2. Loading the resource into memory.
|
|
139
165
|
|
|
140
|
-
What if we want to download the resource ahead of time, but only load into memory when needed?
|
|
166
|
+
What if we want to *download* the resource ahead of time, but only load into memory when needed?
|
|
141
167
|
|
|
142
168
|
The link rel=modulepreload option provides an already existing platform support for this, but the browser complains when no use of the resource is used within a short time span of page load. That doesn't really fit the bill for lazy loading custom elements and other resources.
|
|
143
169
|
|
|
144
|
-
So for this we add
|
|
170
|
+
So for this we add loadingEagerness:
|
|
145
171
|
|
|
146
172
|
```JavaScript
|
|
147
173
|
const observer = new MountObserver({
|
|
148
|
-
|
|
174
|
+
select: 'my-element',
|
|
149
175
|
loadingEagerness: 'eager',
|
|
150
176
|
import: './my-element.js',
|
|
151
|
-
do:{
|
|
152
|
-
|
|
153
|
-
}
|
|
154
|
-
})
|
|
177
|
+
do: ({localName}, {modules}) => customElements.define(localName, modules[0].MyElement),
|
|
178
|
+
});
|
|
155
179
|
```
|
|
156
180
|
|
|
157
181
|
So what this does is only check for the presence of an element with tag name "my-element", and it starts downloading the resource, even before the element has "mounted" based on other criteria.
|
|
@@ -175,7 +199,7 @@ Following an approach similar to the [speculation api](https://developer.chrome.
|
|
|
175
199
|
observer.disconnectedSignal.abort();
|
|
176
200
|
}">
|
|
177
201
|
{
|
|
178
|
-
"
|
|
202
|
+
"select":"my-element",
|
|
179
203
|
"import": [
|
|
180
204
|
["./my-element-small.css", {type: "css"}],
|
|
181
205
|
"./my-element.js",
|
|
@@ -206,7 +230,7 @@ The syntax below is just one, "spit-balling" way this could be done, as an examp
|
|
|
206
230
|
```html
|
|
207
231
|
<script type="mountobserver">
|
|
208
232
|
{
|
|
209
|
-
"
|
|
233
|
+
"select":"my-element",
|
|
210
234
|
"import": [
|
|
211
235
|
["./my-element-small.css", {type: "css"}],
|
|
212
236
|
"./my-element.js",
|
|
@@ -233,7 +257,7 @@ Inside a shadow root, we can plop a script element, also with type "mountobserve
|
|
|
233
257
|
#shadowRoot
|
|
234
258
|
<script id=myMountObserver type=mountobserver>
|
|
235
259
|
{
|
|
236
|
-
"
|
|
260
|
+
"select":"your-element"
|
|
237
261
|
}
|
|
238
262
|
</script>
|
|
239
263
|
```
|
|
@@ -252,15 +276,18 @@ We will come back to some important [additional features](#creating-frameworks-t
|
|
|
252
276
|
|
|
253
277
|
## Binding from a distance
|
|
254
278
|
|
|
255
|
-
It is important to note that "
|
|
279
|
+
It is important to note that "select" is a css query with no restrictions. So something like:
|
|
256
280
|
|
|
257
281
|
```JavaScript
|
|
258
282
|
const observer = new MountObserver({
|
|
259
|
-
|
|
283
|
+
select:'div > p + p ~ span[class$="name"]',
|
|
260
284
|
do:{
|
|
261
285
|
mount: (matchingElement) => {
|
|
262
286
|
//attach some behavior or set some property value or add an event listener, etc.
|
|
263
287
|
matchingElement.textContent = 'hello';
|
|
288
|
+
},
|
|
289
|
+
dismount: (matchingElement) => {
|
|
290
|
+
matchingElement.textContent = 'bye';
|
|
264
291
|
}
|
|
265
292
|
}
|
|
266
293
|
})
|
|
@@ -268,8 +295,78 @@ const observer = new MountObserver({
|
|
|
268
295
|
|
|
269
296
|
... would work.
|
|
270
297
|
|
|
298
|
+
Note that in this example, "do" no longer points to a function. When it did (above), we mentioned this would only be called once per element. **Now it will be called every the conditions flip from not all satisfied to satisfied"**.
|
|
299
|
+
|
|
271
300
|
This would allow developers to create "stylesheet" like capabilities.
|
|
272
301
|
|
|
302
|
+
## Applying properties with assignGingerly
|
|
303
|
+
|
|
304
|
+
For the common use case of setting properties on matching elements, MountObserver provides built-in support for the [assignGingerly](https://github.com/bahrus/assign-gingerly) library. This allows you to declaratively specify properties to apply to elements without writing custom mount callbacks:
|
|
305
|
+
|
|
306
|
+
```JavaScript
|
|
307
|
+
const observer = new MountObserver({
|
|
308
|
+
whereElementMatches: 'input',
|
|
309
|
+
assignGingerly: {
|
|
310
|
+
disabled: true,
|
|
311
|
+
value: 'Default value',
|
|
312
|
+
title: 'This is a tooltip'
|
|
313
|
+
}
|
|
314
|
+
});
|
|
315
|
+
observer.observe(document);
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
This will automatically apply the specified properties to all matching input elements, both existing ones and those added dynamically.
|
|
319
|
+
|
|
320
|
+
### Nested properties with dataset
|
|
321
|
+
|
|
322
|
+
The `assignGingerly` library supports nested property assignment using the `?.` notation. This is particularly useful for setting data attributes:
|
|
323
|
+
|
|
324
|
+
```JavaScript
|
|
325
|
+
const observer = new MountObserver({
|
|
326
|
+
whereElementMatches: 'button',
|
|
327
|
+
assignGingerly: {
|
|
328
|
+
disabled: false,
|
|
329
|
+
'?.dataset.action': 'submit',
|
|
330
|
+
'?.dataset.trackingId': '12345'
|
|
331
|
+
}
|
|
332
|
+
});
|
|
333
|
+
observer.observe(document);
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
The `?.` prefix tells assignGingerly to create nested properties if they don't exist. In this example, `?.dataset.action` will set the `data-action` attribute on the button elements.
|
|
337
|
+
|
|
338
|
+
### Combining with imports
|
|
339
|
+
|
|
340
|
+
You can combine `assignGingerly` with lazy loading to both import resources and set properties:
|
|
341
|
+
|
|
342
|
+
```JavaScript
|
|
343
|
+
const observer = new MountObserver({
|
|
344
|
+
whereElementMatches: 'my-element',
|
|
345
|
+
import: './my-element.js',
|
|
346
|
+
assignGingerly: {
|
|
347
|
+
theme: 'dark',
|
|
348
|
+
'?.dataset.initialized': 'true'
|
|
349
|
+
},
|
|
350
|
+
do: ({localName}, {modules}) => {
|
|
351
|
+
if(!customElements.get(localName)) {
|
|
352
|
+
customElements.define(localName, modules[0].MyElement);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
});
|
|
356
|
+
observer.observe(document);
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
The `assignGingerly` properties are applied after imports are loaded but before the `do` callback is invoked, ensuring that elements are properly configured before any custom initialization logic runs.
|
|
360
|
+
|
|
361
|
+
### Performance benefits
|
|
362
|
+
|
|
363
|
+
Using `assignGingerly` provides several benefits:
|
|
364
|
+
|
|
365
|
+
1. **Lazy loading**: The assign-gingerly library is only loaded when needed (when the `assignGingerly` property is specified)
|
|
366
|
+
2. **Bulk operations**: Properties are applied efficiently to all matching elements
|
|
367
|
+
3. **Declarative**: No need to write custom mount callbacks for simple property assignments
|
|
368
|
+
4. **Consistent**: The same property values are applied uniformly across all matching elements
|
|
369
|
+
|
|
273
370
|
|
|
274
371
|
## Extra lazy loading
|
|
275
372
|
|
|
@@ -279,7 +376,7 @@ However, we could make the loading even more lazy by specifying intersection opt
|
|
|
279
376
|
|
|
280
377
|
```JavaScript
|
|
281
378
|
const observer = new MountObserver({
|
|
282
|
-
|
|
379
|
+
select: 'my-element',
|
|
283
380
|
whereElementIntersectsWith:{
|
|
284
381
|
rootMargin: "0px",
|
|
285
382
|
threshold: 1.0,
|
|
@@ -294,13 +391,13 @@ Unlike traditional CSS @import, CSS Modules don't support specifying different i
|
|
|
294
391
|
|
|
295
392
|
```JavaScript
|
|
296
393
|
const observer = new MountObserver({
|
|
297
|
-
|
|
394
|
+
select: 'div > p + p ~ span[class$="name"]',
|
|
298
395
|
whereMediaMatches: '(max-width: 1250px)',
|
|
299
396
|
whereSizeOfContainerMatches: '(min-width: 700px)',
|
|
300
397
|
whereContainerHas: '[itemprop=isActive][value="true"]',
|
|
301
398
|
whereInstanceOf: [HTMLMarqueeElement], //or ['HTMLMarqueeElement']
|
|
302
399
|
whereLangIn: ['en-GB'],
|
|
303
|
-
|
|
400
|
+
whereConnectiselect:{
|
|
304
401
|
effectiveTypeIn: ["slow-2g"],
|
|
305
402
|
},
|
|
306
403
|
import: ['./my-element-small.css', {type: 'css'}],
|
|
@@ -416,9 +513,9 @@ So the dismount event should provide a "checklist" of all the conditions, and th
|
|
|
416
513
|
```JavaScript
|
|
417
514
|
mediaMatches: true,
|
|
418
515
|
containerMatches: true,
|
|
419
|
-
|
|
516
|
+
satisfiesCustomConditiselect: true,
|
|
420
517
|
whereLangIn: ['en-GB'],
|
|
421
|
-
|
|
518
|
+
whereConnectiselect:{
|
|
422
519
|
effectiveTypeMatches: true
|
|
423
520
|
},
|
|
424
521
|
isIntersecting: false,
|
|
@@ -459,7 +556,7 @@ For the polyfill, we need to support it as follows:
|
|
|
459
556
|
```JavaScript
|
|
460
557
|
const oElement = document.getElementById('myTest');
|
|
461
558
|
const observer = new MountObserver({
|
|
462
|
-
|
|
559
|
+
select:'[itemprop]',
|
|
463
560
|
outside: '[itemscope]'
|
|
464
561
|
do: {
|
|
465
562
|
mount: ({localName}, {modules, observer}) => {
|
|
@@ -531,7 +628,7 @@ Let's focus on the first scenario. It doesn't make sense to use the word "where
|
|
|
531
628
|
```JavaScript
|
|
532
629
|
import {MountObserver} from 'mount-observer/MountObserver.js';
|
|
533
630
|
const mo = new MountObserver({
|
|
534
|
-
|
|
631
|
+
select: '*',
|
|
535
632
|
observedAttrsWhenMounted: ['lang', 'contenteditable']
|
|
536
633
|
});
|
|
537
634
|
|
|
@@ -613,7 +710,7 @@ Using the same expression structure as above, we would end up with this avalanch
|
|
|
613
710
|
```JavaScript
|
|
614
711
|
import {MountObserver} from '../MountObserver.js';
|
|
615
712
|
const mo = new MountObserver({
|
|
616
|
-
|
|
713
|
+
select: '*',
|
|
617
714
|
whereAttr:{
|
|
618
715
|
isIn: [
|
|
619
716
|
'data-my-enhancement',
|
|
@@ -649,7 +746,6 @@ This seems like a much better approach, and is supported by this proposal:
|
|
|
649
746
|
```JavaScript
|
|
650
747
|
import {MountObserver} from '../MountObserver.js';
|
|
651
748
|
const mo = new MountObserver({
|
|
652
|
-
on: '*',
|
|
653
749
|
whereAttr:{
|
|
654
750
|
hasRootIn: ['data', 'enh', 'data-enh'],
|
|
655
751
|
hasBase: 'my-enhancement',
|
|
@@ -670,7 +766,7 @@ MountObserver provides a breakdown of the matching attribute when encountered:
|
|
|
670
766
|
<script type=module>
|
|
671
767
|
import {MountObserver} from '../MountObserver.js';
|
|
672
768
|
const mo = new MountObserver({
|
|
673
|
-
|
|
769
|
+
select: '*',
|
|
674
770
|
whereAttr:{
|
|
675
771
|
hasRootIn: ['data', 'enh', 'data-enh'],
|
|
676
772
|
hasBase: 'my-enhancement',
|
|
@@ -727,7 +823,7 @@ An example of this in the real world can be found with [HTMX](https://htmx.org/d
|
|
|
727
823
|
|
|
728
824
|
```html
|
|
729
825
|
<button hx-post="/example"
|
|
730
|
-
hx-
|
|
826
|
+
hx-select:htmx:config-request="event.detail.parameters.example = 'Hello Scripting!'">
|
|
731
827
|
Post Me!
|
|
732
828
|
</button>
|
|
733
829
|
```
|
|
@@ -736,7 +832,7 @@ To support such syntax, specify the delimiters thusly:
|
|
|
736
832
|
|
|
737
833
|
```JavaScript
|
|
738
834
|
const mo = new MountObserver({
|
|
739
|
-
|
|
835
|
+
select: '*',
|
|
740
836
|
whereAttr:{
|
|
741
837
|
hasRootIn: ['data', 'enh', 'data-enh'],
|
|
742
838
|
hasBase: ['-', 'my-enhancement'],
|
|
@@ -758,7 +854,7 @@ Thus the mountObserver does provide that information to the consumer as well:
|
|
|
758
854
|
|
|
759
855
|
```JavaScript
|
|
760
856
|
const mo = new MountObserver({
|
|
761
|
-
|
|
857
|
+
select: '*',
|
|
762
858
|
whereAttr:{
|
|
763
859
|
hasRootIn: ['data', 'enh', 'data-enh'],
|
|
764
860
|
hasBase: ['-', 'my-enhancement'],
|
|
@@ -1015,7 +1111,7 @@ Just as it is useful to be able lazy load external imports when needed, it would
|
|
|
1015
1111
|
<template id=source-template rel=conditional-stream>
|
|
1016
1112
|
|
|
1017
1113
|
<template mount='{
|
|
1018
|
-
"
|
|
1114
|
+
"select": ":not([defer-loading])",
|
|
1019
1115
|
"loadingEagerness": "eager",
|
|
1020
1116
|
"whereMediaMatches": "(min-width: 700px)",
|
|
1021
1117
|
"whereLangIn": ["en-GB"],
|
|
@@ -1024,7 +1120,7 @@ Just as it is useful to be able lazy load external imports when needed, it would
|
|
|
1024
1120
|
</template>
|
|
1025
1121
|
|
|
1026
1122
|
<template mount='{
|
|
1027
|
-
"
|
|
1123
|
+
"select": ":not([defer-loading])",
|
|
1028
1124
|
"loadingEagerness": "lazy",
|
|
1029
1125
|
"whereMediaMatches": "(max-width: 700px)",
|
|
1030
1126
|
"whereLangIn": ["fr"],
|
|
@@ -1236,7 +1332,4 @@ To keep the api uniform, we hide this discrepancy by pretending the form element
|
|
|
1236
1332
|
// includes both field1 and field2
|
|
1237
1333
|
|
|
1238
1334
|
</script>
|
|
1239
|
-
```
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1335
|
+
```
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Builds a map of attribute names to their coordinates based on whereAttr config
|
|
3
|
+
*/
|
|
4
|
+
export function buildAttrCoordinateMap(whereAttr, isCustomElement) {
|
|
5
|
+
const map = {};
|
|
6
|
+
const rootPrefixes = isCustomElement
|
|
7
|
+
? (whereAttr.hasCERootIn || [])
|
|
8
|
+
: (whereAttr.hasBuiltInRootIn || []);
|
|
9
|
+
// Parse base attribute for custom delimiter
|
|
10
|
+
const { delimiter: baseDelimiter, name: baseName } = parseDelimiter(whereAttr.hasBase);
|
|
11
|
+
// Build attribute names for each prefix
|
|
12
|
+
for (const prefix of rootPrefixes) {
|
|
13
|
+
const baseAttrName = buildAttributeName(prefix, baseName, baseDelimiter);
|
|
14
|
+
// If no branches specified, just the base attribute
|
|
15
|
+
if (!whereAttr.hasBranchIn || whereAttr.hasBranchIn.length === 0) {
|
|
16
|
+
map[baseAttrName] = '0';
|
|
17
|
+
continue;
|
|
18
|
+
}
|
|
19
|
+
// Process each branch
|
|
20
|
+
for (let i = 0; i < whereAttr.hasBranchIn.length; i++) {
|
|
21
|
+
const branch = whereAttr.hasBranchIn[i];
|
|
22
|
+
if (branch === '') {
|
|
23
|
+
// Empty string means base attribute alone is valid
|
|
24
|
+
map[baseAttrName] = '0';
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
if (typeof branch === 'object') {
|
|
28
|
+
// Process branch object
|
|
29
|
+
processBranch(branch, baseAttrName, String(i), map);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return map;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Recursively processes a branch object to build attribute-coordinate mappings
|
|
37
|
+
*/
|
|
38
|
+
function processBranch(branch, parentAttrName, parentCoordinate, map) {
|
|
39
|
+
for (const [key, subBranches] of Object.entries(branch)) {
|
|
40
|
+
const { delimiter, name } = parseDelimiter(key);
|
|
41
|
+
const attrName = parentAttrName + delimiter + name;
|
|
42
|
+
// Process sub-branches
|
|
43
|
+
if (!subBranches || subBranches.length === 0) {
|
|
44
|
+
map[attrName] = parentCoordinate;
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
for (let i = 0; i < subBranches.length; i++) {
|
|
48
|
+
const subBranch = subBranches[i];
|
|
49
|
+
const coordinate = `${parentCoordinate}.${i}`;
|
|
50
|
+
if (subBranch === '') {
|
|
51
|
+
// Empty string means this level alone is valid
|
|
52
|
+
map[attrName] = parentCoordinate;
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
if (typeof subBranch === 'string') {
|
|
56
|
+
// Simple string sub-branch
|
|
57
|
+
const { delimiter: subDelimiter, name: subName } = parseDelimiter(subBranch);
|
|
58
|
+
const subAttrName = attrName + subDelimiter + subName;
|
|
59
|
+
map[subAttrName] = coordinate;
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
if (typeof subBranch === 'object') {
|
|
63
|
+
// Nested object - recursively process
|
|
64
|
+
processBranch(subBranch, attrName, coordinate, map);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Parses a key to extract custom delimiter and name
|
|
71
|
+
*/
|
|
72
|
+
function parseDelimiter(key) {
|
|
73
|
+
const match = key.match(/^\[(.+?)\](.+)$/);
|
|
74
|
+
if (match) {
|
|
75
|
+
return {
|
|
76
|
+
delimiter: match[1],
|
|
77
|
+
name: match[2]
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
return {
|
|
81
|
+
delimiter: '-',
|
|
82
|
+
name: key
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Builds the full attribute name from prefix, base name, and delimiter
|
|
87
|
+
*/
|
|
88
|
+
function buildAttributeName(prefix, baseName, delimiter) {
|
|
89
|
+
if (prefix === '') {
|
|
90
|
+
return baseName;
|
|
91
|
+
}
|
|
92
|
+
return prefix + delimiter + baseName;
|
|
93
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { WhereAttr, BranchValue } from './types.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Represents a mapping from attribute name to coordinate
|
|
5
|
+
*/
|
|
6
|
+
export interface AttrCoordinateMap {
|
|
7
|
+
[attrName: string]: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Builds a map of attribute names to their coordinates based on whereAttr config
|
|
12
|
+
*/
|
|
13
|
+
export function buildAttrCoordinateMap(whereAttr: WhereAttr, isCustomElement: boolean): AttrCoordinateMap {
|
|
14
|
+
const map: AttrCoordinateMap = {};
|
|
15
|
+
const rootPrefixes = isCustomElement
|
|
16
|
+
? (whereAttr.hasCERootIn || [])
|
|
17
|
+
: (whereAttr.hasBuiltInRootIn || []);
|
|
18
|
+
|
|
19
|
+
// Parse base attribute for custom delimiter
|
|
20
|
+
const { delimiter: baseDelimiter, name: baseName } = parseDelimiter(whereAttr.hasBase);
|
|
21
|
+
|
|
22
|
+
// Build attribute names for each prefix
|
|
23
|
+
for (const prefix of rootPrefixes) {
|
|
24
|
+
const baseAttrName = buildAttributeName(prefix, baseName, baseDelimiter);
|
|
25
|
+
|
|
26
|
+
// If no branches specified, just the base attribute
|
|
27
|
+
if (!whereAttr.hasBranchIn || whereAttr.hasBranchIn.length === 0) {
|
|
28
|
+
map[baseAttrName] = '0';
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Process each branch
|
|
33
|
+
for (let i = 0; i < whereAttr.hasBranchIn.length; i++) {
|
|
34
|
+
const branch = whereAttr.hasBranchIn[i];
|
|
35
|
+
|
|
36
|
+
if (branch === '') {
|
|
37
|
+
// Empty string means base attribute alone is valid
|
|
38
|
+
map[baseAttrName] = '0';
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (typeof branch === 'object') {
|
|
43
|
+
// Process branch object
|
|
44
|
+
processBranch(branch, baseAttrName, String(i), map);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return map;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Recursively processes a branch object to build attribute-coordinate mappings
|
|
54
|
+
*/
|
|
55
|
+
function processBranch(
|
|
56
|
+
branch: { [key: string]: BranchValue[] },
|
|
57
|
+
parentAttrName: string,
|
|
58
|
+
parentCoordinate: string,
|
|
59
|
+
map: AttrCoordinateMap
|
|
60
|
+
): void {
|
|
61
|
+
for (const [key, subBranches] of Object.entries(branch)) {
|
|
62
|
+
const { delimiter, name } = parseDelimiter(key);
|
|
63
|
+
const attrName = parentAttrName + delimiter + name;
|
|
64
|
+
|
|
65
|
+
// Process sub-branches
|
|
66
|
+
if (!subBranches || subBranches.length === 0) {
|
|
67
|
+
map[attrName] = parentCoordinate;
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
for (let i = 0; i < subBranches.length; i++) {
|
|
72
|
+
const subBranch = subBranches[i];
|
|
73
|
+
const coordinate = `${parentCoordinate}.${i}`;
|
|
74
|
+
|
|
75
|
+
if (subBranch === '') {
|
|
76
|
+
// Empty string means this level alone is valid
|
|
77
|
+
map[attrName] = parentCoordinate;
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (typeof subBranch === 'string') {
|
|
82
|
+
// Simple string sub-branch
|
|
83
|
+
const { delimiter: subDelimiter, name: subName } = parseDelimiter(subBranch);
|
|
84
|
+
const subAttrName = attrName + subDelimiter + subName;
|
|
85
|
+
map[subAttrName] = coordinate;
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (typeof subBranch === 'object') {
|
|
90
|
+
// Nested object - recursively process
|
|
91
|
+
processBranch(subBranch, attrName, coordinate, map);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Parses a key to extract custom delimiter and name
|
|
99
|
+
*/
|
|
100
|
+
function parseDelimiter(key: string): { delimiter: string; name: string } {
|
|
101
|
+
const match = key.match(/^\[(.+?)\](.+)$/);
|
|
102
|
+
if (match) {
|
|
103
|
+
return {
|
|
104
|
+
delimiter: match[1],
|
|
105
|
+
name: match[2]
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
return {
|
|
109
|
+
delimiter: '-',
|
|
110
|
+
name: key
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Builds the full attribute name from prefix, base name, and delimiter
|
|
116
|
+
*/
|
|
117
|
+
function buildAttributeName(prefix: string, baseName: string, delimiter: string): string {
|
|
118
|
+
if (prefix === '') {
|
|
119
|
+
return baseName;
|
|
120
|
+
}
|
|
121
|
+
return prefix + delimiter + baseName;
|
|
122
|
+
}
|
package/constants.js
ADDED
package/constants.ts
ADDED
package/index.js
ADDED
package/index.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// Main entry point for MountObserver v2
|
|
2
|
+
export { MountObserver } from './MountObserver.js';
|
|
3
|
+
export type {
|
|
4
|
+
MountInit,
|
|
5
|
+
MountObserverOptions,
|
|
6
|
+
IMountObserver,
|
|
7
|
+
MountContext,
|
|
8
|
+
DoCallback,
|
|
9
|
+
DoCallbacks,
|
|
10
|
+
ImportSpec,
|
|
11
|
+
IMountEvent,
|
|
12
|
+
IDismountEvent
|
|
13
|
+
} from './types.js';
|
|
14
|
+
export {
|
|
15
|
+
mountEventName,
|
|
16
|
+
dismountEventName,
|
|
17
|
+
disconnectEventName,
|
|
18
|
+
loadEventName
|
|
19
|
+
} from './constants.js';
|