@mintjamsinc/ichigojs 0.1.75 → 0.1.77
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/README.md +135 -5
- package/dist/ichigo.cjs +918 -63
- package/dist/ichigo.cjs.map +1 -1
- package/dist/ichigo.esm.js +918 -64
- package/dist/ichigo.esm.js.map +1 -1
- package/dist/ichigo.esm.min.js +1 -1
- package/dist/ichigo.min.cjs +1 -1
- package/dist/ichigo.umd.js +918 -63
- package/dist/ichigo.umd.js.map +1 -1
- package/dist/ichigo.umd.min.js +1 -1
- package/dist/types/ichigo/VApplication.d.ts +19 -1
- package/dist/types/ichigo/components/IchigoComponentOptions.d.ts +49 -5
- package/dist/types/ichigo/components/IchigoElement.d.ts +55 -7
- package/dist/types/ichigo/components/IchigoElementRegistry.d.ts +22 -0
- package/dist/types/ichigo/components/defineComponent.d.ts +6 -3
- package/dist/types/ichigo/directives/VSlotOutletDirective.d.ts +114 -0
- package/dist/types/ichigo/util/DisplayString.d.ts +12 -0
- package/dist/types/index.d.ts +2 -1
- package/package.json +1 -1
package/dist/ichigo.esm.js
CHANGED
|
@@ -7505,6 +7505,14 @@ class VBindDirective {
|
|
|
7505
7505
|
* @inheritdoc
|
|
7506
7506
|
*/
|
|
7507
7507
|
get onMount() {
|
|
7508
|
+
// A constant expression (e.g. :label="'Apple'") has no reactive
|
|
7509
|
+
// identifiers, so the identifier-driven update cycle never applies it.
|
|
7510
|
+
// Render it once at mount instead.
|
|
7511
|
+
if (this.#evaluator && this.#evaluator.dependentIdentifiers.length === 0) {
|
|
7512
|
+
return () => {
|
|
7513
|
+
this.#render();
|
|
7514
|
+
};
|
|
7515
|
+
}
|
|
7508
7516
|
return undefined;
|
|
7509
7517
|
}
|
|
7510
7518
|
/**
|
|
@@ -8920,6 +8928,344 @@ class VBindings {
|
|
|
8920
8928
|
}
|
|
8921
8929
|
}
|
|
8922
8930
|
|
|
8931
|
+
// Copyright (c) 2025 MintJams Inc. Licensed under MIT License.
|
|
8932
|
+
/**
|
|
8933
|
+
* Registry of custom element tag names defined via defineComponent().
|
|
8934
|
+
*
|
|
8935
|
+
* VNode consults this registry during compilation to recognize component
|
|
8936
|
+
* boundaries: the children of an already-expanded ichigo component belong to
|
|
8937
|
+
* the component's own VApplication and must not be compiled by the parent
|
|
8938
|
+
* application (doing so would strip the component's directives and evaluate
|
|
8939
|
+
* its template expressions in the wrong scope).
|
|
8940
|
+
*/
|
|
8941
|
+
class IchigoElementRegistry {
|
|
8942
|
+
/**
|
|
8943
|
+
* Registered tag names, stored uppercased to match Node.nodeName.
|
|
8944
|
+
*/
|
|
8945
|
+
static #tags = new Set();
|
|
8946
|
+
/**
|
|
8947
|
+
* Registers a component tag name. Called by defineComponent().
|
|
8948
|
+
* @param tagName The custom element tag name (case-insensitive).
|
|
8949
|
+
*/
|
|
8950
|
+
static add(tagName) {
|
|
8951
|
+
this.#tags.add(tagName.toUpperCase());
|
|
8952
|
+
}
|
|
8953
|
+
/**
|
|
8954
|
+
* Checks whether a tag name belongs to a component defined via defineComponent().
|
|
8955
|
+
* @param tagName The tag name to check (case-insensitive; nodeName is accepted as-is).
|
|
8956
|
+
*/
|
|
8957
|
+
static has(tagName) {
|
|
8958
|
+
return this.#tags.has(tagName.toUpperCase());
|
|
8959
|
+
}
|
|
8960
|
+
}
|
|
8961
|
+
|
|
8962
|
+
// Copyright (c) 2025 MintJams Inc. Licensed under MIT License.
|
|
8963
|
+
/**
|
|
8964
|
+
* Renders a scoped slot outlet: a `<slot>` element inside a component template
|
|
8965
|
+
* whose host element received a matching `<template v-slot:name="scope">` from
|
|
8966
|
+
* the parent.
|
|
8967
|
+
*
|
|
8968
|
+
* The slot content is compiled in the PARENT application's scope (the scope in
|
|
8969
|
+
* which the template was authored), extended with the slot props — the values
|
|
8970
|
+
* bound on the `<slot>` outlet with `v-bind` / `:` — exposed under the single
|
|
8971
|
+
* scope variable declared in `v-slot` (e.g. `v-slot:item="row"` makes
|
|
8972
|
+
* `row.node`, `row.index`, ... available):
|
|
8973
|
+
*
|
|
8974
|
+
* ```html
|
|
8975
|
+
* <!-- component template -->
|
|
8976
|
+
* <li v-for="(node, i) of items">
|
|
8977
|
+
* <slot name="item" :node="node" :index="i">fallback</slot>
|
|
8978
|
+
* </li>
|
|
8979
|
+
*
|
|
8980
|
+
* <!-- usage -->
|
|
8981
|
+
* <my-tree :items="nodes">
|
|
8982
|
+
* <template v-slot:item="row">{{ row.node.label }} ({{ appTitle }})</template>
|
|
8983
|
+
* </my-tree>
|
|
8984
|
+
* ```
|
|
8985
|
+
*
|
|
8986
|
+
* This directive is created only when a scoped template for the slot's name
|
|
8987
|
+
* exists (see {@link VSlotOutletDirective.tryCreate}); otherwise `<slot>`
|
|
8988
|
+
* elements keep their existing behavior (static distribution at expansion,
|
|
8989
|
+
* or fallback content compiled in the component scope).
|
|
8990
|
+
*
|
|
8991
|
+
* Rendering happens synchronously in onMount — before the component
|
|
8992
|
+
* application would compile the slot's fallback children — by replacing the
|
|
8993
|
+
* `<slot>` element with a clone of the scoped template's content. The content
|
|
8994
|
+
* VNode is attached under the host element's VNode in the parent application,
|
|
8995
|
+
* so parent-side data changes flow through the normal update cycle; slot-prop
|
|
8996
|
+
* changes on the component side are pushed by this directive's domUpdater.
|
|
8997
|
+
*/
|
|
8998
|
+
class VSlotOutletDirective {
|
|
8999
|
+
/**
|
|
9000
|
+
* The virtual node of the `<slot>` element (in the component application).
|
|
9001
|
+
*/
|
|
9002
|
+
#vNode;
|
|
9003
|
+
/**
|
|
9004
|
+
* The captured scoped template and its scope variable name.
|
|
9005
|
+
*/
|
|
9006
|
+
#entry;
|
|
9007
|
+
/**
|
|
9008
|
+
* The host element's VNode in the parent application. Slot content is
|
|
9009
|
+
* compiled against this node's application and bindings.
|
|
9010
|
+
*/
|
|
9011
|
+
#hostVNode;
|
|
9012
|
+
/**
|
|
9013
|
+
* The slot props: camelized names and evaluators for the `v-bind` / `:`
|
|
9014
|
+
* attributes found on the `<slot>` outlet (evaluated in component scope).
|
|
9015
|
+
*/
|
|
9016
|
+
#props = [];
|
|
9017
|
+
/**
|
|
9018
|
+
* The bindings for the slot content: a child scope of the parent
|
|
9019
|
+
* application's bindings holding the scope variable.
|
|
9020
|
+
*/
|
|
9021
|
+
#scopeBindings;
|
|
9022
|
+
/**
|
|
9023
|
+
* The VNode tree of the rendered slot content (owned by the parent application).
|
|
9024
|
+
*/
|
|
9025
|
+
#contentVNode;
|
|
9026
|
+
/**
|
|
9027
|
+
* Guards against double destruction (the content VNode is reachable from
|
|
9028
|
+
* both the parent application's tree and this directive).
|
|
9029
|
+
*/
|
|
9030
|
+
#destroyed = false;
|
|
9031
|
+
/**
|
|
9032
|
+
* Tracks host elements that already logged the "no host context" warning.
|
|
9033
|
+
*/
|
|
9034
|
+
static #warnedNoHost = new WeakSet();
|
|
9035
|
+
/**
|
|
9036
|
+
* Creates a slot outlet directive for the given `<slot>` VNode when a
|
|
9037
|
+
* scoped template for its slot name was captured on the host element.
|
|
9038
|
+
* Returns undefined when there is nothing to do (no component host, no
|
|
9039
|
+
* scoped template for this name) so the `<slot>` element keeps its
|
|
9040
|
+
* existing behavior.
|
|
9041
|
+
*/
|
|
9042
|
+
static tryCreate(vNode) {
|
|
9043
|
+
const host = vNode.vApplication.hostElement;
|
|
9044
|
+
if (!host) {
|
|
9045
|
+
return undefined;
|
|
9046
|
+
}
|
|
9047
|
+
const scopedSlots = host._ichigoScopedSlots;
|
|
9048
|
+
if (!scopedSlots || scopedSlots.size === 0) {
|
|
9049
|
+
return undefined;
|
|
9050
|
+
}
|
|
9051
|
+
const element = vNode.node;
|
|
9052
|
+
const slotName = element.getAttribute('name') || 'default';
|
|
9053
|
+
const entry = scopedSlots.get(slotName);
|
|
9054
|
+
if (!entry) {
|
|
9055
|
+
return undefined;
|
|
9056
|
+
}
|
|
9057
|
+
const hostVNode = host._ichigoHost?.vNode;
|
|
9058
|
+
if (!hostVNode) {
|
|
9059
|
+
// The scoped template cannot be rendered without the parent scope.
|
|
9060
|
+
// Leave the <slot> alone (fallback content renders in component scope).
|
|
9061
|
+
if (!VSlotOutletDirective.#warnedNoHost.has(host)) {
|
|
9062
|
+
VSlotOutletDirective.#warnedNoHost.add(host);
|
|
9063
|
+
console.warn(`[ichigo] <${host.tagName.toLowerCase()}>: a scoped slot template for '${slotName}' was provided, but the component has no parent application scope to render it in. The fallback content is used instead.`);
|
|
9064
|
+
}
|
|
9065
|
+
return undefined;
|
|
9066
|
+
}
|
|
9067
|
+
return new VSlotOutletDirective(vNode, entry, hostVNode);
|
|
9068
|
+
}
|
|
9069
|
+
constructor(vNode, entry, hostVNode) {
|
|
9070
|
+
this.#vNode = vNode;
|
|
9071
|
+
this.#entry = entry;
|
|
9072
|
+
this.#hostVNode = hostVNode;
|
|
9073
|
+
// Collect and consume the slot-prop bindings (v-bind / :) so the
|
|
9074
|
+
// generic attribute parsing does not also process them.
|
|
9075
|
+
const element = vNode.node;
|
|
9076
|
+
for (const attr of Array.from(element.attributes)) {
|
|
9077
|
+
let propName;
|
|
9078
|
+
if (attr.name.startsWith('v-bind:')) {
|
|
9079
|
+
propName = attr.name.substring(7);
|
|
9080
|
+
}
|
|
9081
|
+
else if (attr.name.startsWith(':')) {
|
|
9082
|
+
propName = attr.name.substring(1);
|
|
9083
|
+
}
|
|
9084
|
+
if (!propName) {
|
|
9085
|
+
continue;
|
|
9086
|
+
}
|
|
9087
|
+
const evaluator = attr.value && vNode.bindings
|
|
9088
|
+
? ExpressionEvaluator.create(attr.value, vNode.bindings, vNode.vApplication.functionDependencies)
|
|
9089
|
+
: undefined;
|
|
9090
|
+
this.#props.push({ name: this.#kebabToCamel(propName), evaluator });
|
|
9091
|
+
element.removeAttribute(attr.name);
|
|
9092
|
+
}
|
|
9093
|
+
}
|
|
9094
|
+
/**
|
|
9095
|
+
* @inheritdoc
|
|
9096
|
+
*/
|
|
9097
|
+
get name() {
|
|
9098
|
+
return 'slot-outlet';
|
|
9099
|
+
}
|
|
9100
|
+
/**
|
|
9101
|
+
* @inheritdoc
|
|
9102
|
+
*/
|
|
9103
|
+
get vNode() {
|
|
9104
|
+
return this.#vNode;
|
|
9105
|
+
}
|
|
9106
|
+
/**
|
|
9107
|
+
* @inheritdoc
|
|
9108
|
+
*/
|
|
9109
|
+
get needsAnchor() {
|
|
9110
|
+
return false;
|
|
9111
|
+
}
|
|
9112
|
+
/**
|
|
9113
|
+
* @inheritdoc
|
|
9114
|
+
*/
|
|
9115
|
+
get bindingsPreparer() {
|
|
9116
|
+
return undefined;
|
|
9117
|
+
}
|
|
9118
|
+
/**
|
|
9119
|
+
* @inheritdoc
|
|
9120
|
+
*/
|
|
9121
|
+
get domUpdater() {
|
|
9122
|
+
const identifiers = this.dependentIdentifiers;
|
|
9123
|
+
const updater = {
|
|
9124
|
+
get dependentIdentifiers() {
|
|
9125
|
+
return identifiers;
|
|
9126
|
+
},
|
|
9127
|
+
applyToDOM: () => {
|
|
9128
|
+
this.#refresh();
|
|
9129
|
+
}
|
|
9130
|
+
};
|
|
9131
|
+
return updater;
|
|
9132
|
+
}
|
|
9133
|
+
/**
|
|
9134
|
+
* @inheritdoc
|
|
9135
|
+
*/
|
|
9136
|
+
get templatize() {
|
|
9137
|
+
return false;
|
|
9138
|
+
}
|
|
9139
|
+
/**
|
|
9140
|
+
* @inheritdoc
|
|
9141
|
+
*/
|
|
9142
|
+
get dependentIdentifiers() {
|
|
9143
|
+
const ids = new Set();
|
|
9144
|
+
for (const prop of this.#props) {
|
|
9145
|
+
prop.evaluator?.dependentIdentifiers.forEach(id => ids.add(id));
|
|
9146
|
+
}
|
|
9147
|
+
return Array.from(ids);
|
|
9148
|
+
}
|
|
9149
|
+
/**
|
|
9150
|
+
* @inheritdoc
|
|
9151
|
+
*
|
|
9152
|
+
* The initial render happens here — synchronously during VNode
|
|
9153
|
+
* construction, after the directives are created and BEFORE the component
|
|
9154
|
+
* application compiles the slot element's children. This guarantees the
|
|
9155
|
+
* outlet renders exactly once regardless of whether its slot props have
|
|
9156
|
+
* reactive identifiers, and removes the fallback children before they
|
|
9157
|
+
* could be compiled in the component scope.
|
|
9158
|
+
*/
|
|
9159
|
+
get onMount() {
|
|
9160
|
+
return () => {
|
|
9161
|
+
this.#render();
|
|
9162
|
+
};
|
|
9163
|
+
}
|
|
9164
|
+
/**
|
|
9165
|
+
* @inheritdoc
|
|
9166
|
+
*/
|
|
9167
|
+
get onMounted() {
|
|
9168
|
+
return undefined;
|
|
9169
|
+
}
|
|
9170
|
+
/**
|
|
9171
|
+
* @inheritdoc
|
|
9172
|
+
*/
|
|
9173
|
+
get onUpdate() {
|
|
9174
|
+
return undefined;
|
|
9175
|
+
}
|
|
9176
|
+
/**
|
|
9177
|
+
* @inheritdoc
|
|
9178
|
+
*/
|
|
9179
|
+
get onUpdated() {
|
|
9180
|
+
return undefined;
|
|
9181
|
+
}
|
|
9182
|
+
/**
|
|
9183
|
+
* @inheritdoc
|
|
9184
|
+
*/
|
|
9185
|
+
get onUnmount() {
|
|
9186
|
+
return undefined;
|
|
9187
|
+
}
|
|
9188
|
+
/**
|
|
9189
|
+
* @inheritdoc
|
|
9190
|
+
*/
|
|
9191
|
+
get onUnmounted() {
|
|
9192
|
+
return undefined;
|
|
9193
|
+
}
|
|
9194
|
+
/**
|
|
9195
|
+
* @inheritdoc
|
|
9196
|
+
*/
|
|
9197
|
+
destroy() {
|
|
9198
|
+
if (this.#destroyed) {
|
|
9199
|
+
return;
|
|
9200
|
+
}
|
|
9201
|
+
this.#destroyed = true;
|
|
9202
|
+
// Destroy the content VNode so its dependency registrations on the
|
|
9203
|
+
// host VNode are released (critical for v-for rows that come and go).
|
|
9204
|
+
// The content DOM nodes live inside the component's subtree and are
|
|
9205
|
+
// removed together with it.
|
|
9206
|
+
this.#contentVNode?.destroy();
|
|
9207
|
+
this.#contentVNode = undefined;
|
|
9208
|
+
this.#scopeBindings = undefined;
|
|
9209
|
+
}
|
|
9210
|
+
/**
|
|
9211
|
+
* Renders the scoped slot content in place of the `<slot>` element.
|
|
9212
|
+
*/
|
|
9213
|
+
#render() {
|
|
9214
|
+
const slot = this.#vNode.node;
|
|
9215
|
+
// Scope: parent application's bindings + the scope variable
|
|
9216
|
+
this.#scopeBindings = new VBindings({ parent: this.#hostVNode.bindings });
|
|
9217
|
+
if (this.#entry.scopeVar) {
|
|
9218
|
+
this.#scopeBindings.setLocal(this.#entry.scopeVar, this.#buildSlotProps());
|
|
9219
|
+
}
|
|
9220
|
+
// Clone the scoped template's content and create its VNode tree BEFORE
|
|
9221
|
+
// inserting into the DOM, so components inside the slot content are
|
|
9222
|
+
// compiled before their connectedCallback can expand them.
|
|
9223
|
+
// (Created through the parent application to keep the VNode import
|
|
9224
|
+
// type-only; a direct `new VNode` here would close a module cycle
|
|
9225
|
+
// VNode -> VDirectiveManager -> VSlotOutletDirective -> VNode.)
|
|
9226
|
+
const fragment = this.#entry.template.content.cloneNode(true);
|
|
9227
|
+
this.#contentVNode = this.#hostVNode.vApplication.createVNode({
|
|
9228
|
+
node: fragment,
|
|
9229
|
+
vApplication: this.#hostVNode.vApplication,
|
|
9230
|
+
parentVNode: this.#hostVNode,
|
|
9231
|
+
bindings: this.#scopeBindings
|
|
9232
|
+
});
|
|
9233
|
+
// Drop the fallback children (a scoped template supersedes them), then
|
|
9234
|
+
// swap the <slot> element for the content.
|
|
9235
|
+
while (slot.firstChild) {
|
|
9236
|
+
slot.removeChild(slot.firstChild);
|
|
9237
|
+
}
|
|
9238
|
+
slot.replaceWith(fragment);
|
|
9239
|
+
// Initial rendering
|
|
9240
|
+
this.#contentVNode.forceUpdate();
|
|
9241
|
+
}
|
|
9242
|
+
/**
|
|
9243
|
+
* Re-evaluates the slot props (component-side change) and pushes them into
|
|
9244
|
+
* the content scope.
|
|
9245
|
+
*/
|
|
9246
|
+
#refresh() {
|
|
9247
|
+
if (!this.#contentVNode || !this.#scopeBindings || !this.#entry.scopeVar) {
|
|
9248
|
+
return;
|
|
9249
|
+
}
|
|
9250
|
+
this.#scopeBindings.setLocal(this.#entry.scopeVar, this.#buildSlotProps());
|
|
9251
|
+
this.#contentVNode.forceUpdate();
|
|
9252
|
+
}
|
|
9253
|
+
/**
|
|
9254
|
+
* Builds the slot props object from the outlet's `v-bind` attributes,
|
|
9255
|
+
* evaluated in the component scope.
|
|
9256
|
+
*/
|
|
9257
|
+
#buildSlotProps() {
|
|
9258
|
+
const props = {};
|
|
9259
|
+
for (const prop of this.#props) {
|
|
9260
|
+
props[prop.name] = prop.evaluator?.evaluate();
|
|
9261
|
+
}
|
|
9262
|
+
return props;
|
|
9263
|
+
}
|
|
9264
|
+
#kebabToCamel(str) {
|
|
9265
|
+
return str.replace(/-([a-z0-9])/g, (_, c) => c.toUpperCase());
|
|
9266
|
+
}
|
|
9267
|
+
}
|
|
9268
|
+
|
|
8923
9269
|
// Copyright (c) 2025 MintJams Inc. Licensed under MIT License.
|
|
8924
9270
|
/**
|
|
8925
9271
|
* Manages directives associated with a virtual node (VNode).
|
|
@@ -9086,6 +9432,24 @@ class VDirectiveManager {
|
|
|
9086
9432
|
}
|
|
9087
9433
|
#parseDirectives() {
|
|
9088
9434
|
const element = this.#vNode.node;
|
|
9435
|
+
// Parse directives from attributes
|
|
9436
|
+
const directives = [];
|
|
9437
|
+
// Scoped slot outlet: a <slot> element (without structural directives)
|
|
9438
|
+
// inside a component whose host received a matching <template v-slot:...>.
|
|
9439
|
+
// This is an element-based directive, created before the generic
|
|
9440
|
+
// attribute parsing so it can consume the slot-prop bindings (:x).
|
|
9441
|
+
// A <slot> carrying v-if / v-for is handled structurally first; the
|
|
9442
|
+
// outlet is then created when the clone is compiled.
|
|
9443
|
+
if (element.tagName === 'SLOT' &&
|
|
9444
|
+
!element.hasAttribute(StandardDirectiveName.V_FOR) &&
|
|
9445
|
+
!element.hasAttribute(StandardDirectiveName.V_IF) &&
|
|
9446
|
+
!element.hasAttribute(StandardDirectiveName.V_ELSE_IF) &&
|
|
9447
|
+
!element.hasAttribute(StandardDirectiveName.V_ELSE)) {
|
|
9448
|
+
const outlet = VSlotOutletDirective.tryCreate(this.#vNode);
|
|
9449
|
+
if (outlet) {
|
|
9450
|
+
directives.push(outlet);
|
|
9451
|
+
}
|
|
9452
|
+
}
|
|
9089
9453
|
// Collect relevant attributes
|
|
9090
9454
|
// For v-for, v-if, v-else-if, v-else: process only the structural directive on the template element.
|
|
9091
9455
|
// Other directives (@click, :class, etc.) will be processed on the cloned/rendered elements.
|
|
@@ -9121,8 +9485,6 @@ class VDirectiveManager {
|
|
|
9121
9485
|
else {
|
|
9122
9486
|
attributes.push(...Array.from(element.attributes));
|
|
9123
9487
|
}
|
|
9124
|
-
// Parse directives from attributes
|
|
9125
|
-
const directives = [];
|
|
9126
9488
|
for (const attribute of attributes) {
|
|
9127
9489
|
// Create a context for parsing the directive
|
|
9128
9490
|
const context = {
|
|
@@ -9170,6 +9532,83 @@ class VDirectiveManager {
|
|
|
9170
9532
|
}
|
|
9171
9533
|
}
|
|
9172
9534
|
|
|
9535
|
+
// Copyright (c) 2025 MintJams Inc. Licensed under MIT License.
|
|
9536
|
+
/**
|
|
9537
|
+
* The default Object.prototype.toString, used to detect plain objects that
|
|
9538
|
+
* have not overridden their string representation.
|
|
9539
|
+
*/
|
|
9540
|
+
const objectToString = Object.prototype.toString;
|
|
9541
|
+
/**
|
|
9542
|
+
* Checks if the value is a non-null object.
|
|
9543
|
+
*/
|
|
9544
|
+
function isObject(value) {
|
|
9545
|
+
return value !== null && typeof value === 'object';
|
|
9546
|
+
}
|
|
9547
|
+
/**
|
|
9548
|
+
* Checks if the value is a plain object (i.e. `[object Object]`).
|
|
9549
|
+
*/
|
|
9550
|
+
function isPlainObject(value) {
|
|
9551
|
+
return objectToString.call(value) === '[object Object]';
|
|
9552
|
+
}
|
|
9553
|
+
/**
|
|
9554
|
+
* Converts a symbol to a readable string, leaving other values untouched.
|
|
9555
|
+
* Used when rendering Map keys and Set entries.
|
|
9556
|
+
*/
|
|
9557
|
+
function stringifySymbol(value, index = '') {
|
|
9558
|
+
return typeof value === 'symbol' ? `Symbol(${value.description ?? index})` : value;
|
|
9559
|
+
}
|
|
9560
|
+
/**
|
|
9561
|
+
* JSON.stringify replacer that renders values JSON cannot represent natively
|
|
9562
|
+
* (Map, Set, Symbol, and non-plain objects) in a readable form.
|
|
9563
|
+
*/
|
|
9564
|
+
function replacer(_key, value) {
|
|
9565
|
+
if (value instanceof Map) {
|
|
9566
|
+
return {
|
|
9567
|
+
[`Map(${value.size})`]: [...value.entries()].reduce((entries, [key, val], i) => {
|
|
9568
|
+
entries[`${stringifySymbol(key, i)} =>`] = val;
|
|
9569
|
+
return entries;
|
|
9570
|
+
}, {}),
|
|
9571
|
+
};
|
|
9572
|
+
}
|
|
9573
|
+
if (value instanceof Set) {
|
|
9574
|
+
return {
|
|
9575
|
+
[`Set(${value.size})`]: [...value.values()].map(v => stringifySymbol(v)),
|
|
9576
|
+
};
|
|
9577
|
+
}
|
|
9578
|
+
if (typeof value === 'symbol') {
|
|
9579
|
+
return stringifySymbol(value);
|
|
9580
|
+
}
|
|
9581
|
+
if (isObject(value) && !Array.isArray(value) && !isPlainObject(value)) {
|
|
9582
|
+
// Non-plain objects (Date, RegExp, DOM nodes, ...) render via their own toString
|
|
9583
|
+
return String(value);
|
|
9584
|
+
}
|
|
9585
|
+
return value;
|
|
9586
|
+
}
|
|
9587
|
+
/**
|
|
9588
|
+
* Converts a value to a string suitable for display in rendered output,
|
|
9589
|
+
* following Vue's `toDisplayString` semantics:
|
|
9590
|
+
* - strings are returned as-is
|
|
9591
|
+
* - null and undefined become an empty string
|
|
9592
|
+
* - arrays and plain objects (without a custom toString) are rendered as
|
|
9593
|
+
* pretty-printed JSON, with Map/Set/Symbol entries made readable
|
|
9594
|
+
* - everything else is converted with String()
|
|
9595
|
+
* @param value The value to convert.
|
|
9596
|
+
* @returns The display string for the value.
|
|
9597
|
+
*/
|
|
9598
|
+
function toDisplayString(value) {
|
|
9599
|
+
if (typeof value === 'string') {
|
|
9600
|
+
return value;
|
|
9601
|
+
}
|
|
9602
|
+
if (value === null || value === undefined) {
|
|
9603
|
+
return '';
|
|
9604
|
+
}
|
|
9605
|
+
if (Array.isArray(value) ||
|
|
9606
|
+
(isObject(value) && (value.toString === objectToString || typeof value.toString !== 'function'))) {
|
|
9607
|
+
return JSON.stringify(value, replacer, 2);
|
|
9608
|
+
}
|
|
9609
|
+
return String(value);
|
|
9610
|
+
}
|
|
9611
|
+
|
|
9173
9612
|
// Copyright (c) 2025 MintJams Inc. Licensed under MIT License.
|
|
9174
9613
|
/**
|
|
9175
9614
|
* List of standard JavaScript global objects that should be available in expressions.
|
|
@@ -9290,8 +9729,9 @@ class VTextEvaluator {
|
|
|
9290
9729
|
return GLOBAL_OBJECTS[name];
|
|
9291
9730
|
return undefined;
|
|
9292
9731
|
});
|
|
9293
|
-
// Evaluate the expression and replace {{...}} in the text
|
|
9294
|
-
|
|
9732
|
+
// Evaluate the expression and replace {{...}} in the text.
|
|
9733
|
+
// A function replacement keeps `$`-patterns in the value literal.
|
|
9734
|
+
result = result.replace(matches[i][0], () => toDisplayString(evaluator.func(...values)));
|
|
9295
9735
|
});
|
|
9296
9736
|
return result;
|
|
9297
9737
|
};
|
|
@@ -9437,7 +9877,14 @@ class VNode {
|
|
|
9437
9877
|
}
|
|
9438
9878
|
else if (this.#nodeType === Node.ELEMENT_NODE) {
|
|
9439
9879
|
// If the node is an element, initialize directives and child nodes
|
|
9440
|
-
this.#node;
|
|
9880
|
+
const element = this.#node;
|
|
9881
|
+
// If the element is a component defined via defineComponent(), install
|
|
9882
|
+
// the host context so component-boundary features (e.g. scoped slot
|
|
9883
|
+
// outlets) can reach back into this application's scope.
|
|
9884
|
+
const isComponent = IchigoElementRegistry.has(this.#nodeName);
|
|
9885
|
+
if (isComponent) {
|
|
9886
|
+
element._ichigoHost = { vNode: this };
|
|
9887
|
+
}
|
|
9441
9888
|
// Initialize child virtual nodes
|
|
9442
9889
|
this.#childVNodes = [];
|
|
9443
9890
|
// Initialize directive manager
|
|
@@ -9450,8 +9897,19 @@ class VNode {
|
|
|
9450
9897
|
d.onMount?.();
|
|
9451
9898
|
});
|
|
9452
9899
|
}
|
|
9900
|
+
// A component whose template is already expanded compiles its own
|
|
9901
|
+
// internals with its own VApplication — this (parent) application must
|
|
9902
|
+
// not adopt them (that would strip the component's directives and
|
|
9903
|
+
// evaluate its expressions in the wrong scope). An unexpanded component
|
|
9904
|
+
// element only contains authored slot content, which does belong to
|
|
9905
|
+
// this application's scope and is compiled normally.
|
|
9906
|
+
const isExpandedComponent = isComponent && element._ichigoExpanded === true;
|
|
9907
|
+
if (isExpandedComponent) {
|
|
9908
|
+
this.#vApplication.logManager.getLogger(this.constructor.name)
|
|
9909
|
+
.debug(`Skipped compiling internals of expanded component <${this.#nodeName.toLowerCase()}>`);
|
|
9910
|
+
}
|
|
9453
9911
|
// Create child virtual nodes if template preservation is not required
|
|
9454
|
-
if (!this.#templatized) {
|
|
9912
|
+
if (!this.#templatized && !isExpandedComponent) {
|
|
9455
9913
|
for (const childNode of Array.from(this.#node.childNodes)) {
|
|
9456
9914
|
new VNode({
|
|
9457
9915
|
node: childNode,
|
|
@@ -11489,17 +11947,45 @@ class VModelDirective {
|
|
|
11489
11947
|
* The modifiers for this v-model directive (e.g., "lazy", "number", "trim")
|
|
11490
11948
|
*/
|
|
11491
11949
|
#modifiers = new Set();
|
|
11950
|
+
/**
|
|
11951
|
+
* The argument of the directive (e.g. "size" for `v-model:size`), camelized.
|
|
11952
|
+
* Only meaningful on components; selects the target prop and update event
|
|
11953
|
+
* (prop `size` + event `update:size`). Undefined = the default `modelValue`.
|
|
11954
|
+
*/
|
|
11955
|
+
#arg;
|
|
11956
|
+
/**
|
|
11957
|
+
* Ensures the "v-model targets an undeclared prop" warning fires only once.
|
|
11958
|
+
*/
|
|
11959
|
+
#warnedUndeclaredProp = false;
|
|
11492
11960
|
/**
|
|
11493
11961
|
* @param context The context for parsing the directive.
|
|
11494
11962
|
*/
|
|
11495
11963
|
constructor(context) {
|
|
11496
11964
|
this.#vNode = context.vNode;
|
|
11497
|
-
// Extract modifiers from the directive name
|
|
11498
|
-
// e.g
|
|
11965
|
+
// Extract the optional argument and modifiers from the directive name
|
|
11966
|
+
// e.g. "v-model" / "v-model.lazy.trim" / "v-model:size" / "v-model:size.number"
|
|
11499
11967
|
const attrName = context.attribute.name;
|
|
11500
|
-
|
|
11501
|
-
|
|
11502
|
-
|
|
11968
|
+
let rest = attrName.substring(StandardDirectiveName.V_MODEL.length);
|
|
11969
|
+
if (rest.startsWith(':')) {
|
|
11970
|
+
const dotIndex = rest.indexOf('.');
|
|
11971
|
+
const rawArg = dotIndex === -1 ? rest.substring(1) : rest.substring(1, dotIndex);
|
|
11972
|
+
if (rawArg) {
|
|
11973
|
+
// HTML attribute names are lowercased by the browser, so a
|
|
11974
|
+
// camelCase prop is addressed with kebab-case (v-model:inner-title).
|
|
11975
|
+
this.#arg = rawArg.replace(/-([a-z0-9])/g, (_, c) => c.toUpperCase());
|
|
11976
|
+
}
|
|
11977
|
+
rest = dotIndex === -1 ? '' : rest.substring(dotIndex);
|
|
11978
|
+
}
|
|
11979
|
+
if (rest.startsWith('.')) {
|
|
11980
|
+
rest.split('.').slice(1).forEach(mod => this.#modifiers.add(mod));
|
|
11981
|
+
}
|
|
11982
|
+
const element = context.vNode.node;
|
|
11983
|
+
if (this.#arg && !this.#isComponentElement(element)) {
|
|
11984
|
+
console.warn(`[ichigo] v-model:${this.#arg} is only supported on components; the argument is ignored on <${element.tagName.toLowerCase()}>`);
|
|
11985
|
+
this.#arg = undefined;
|
|
11986
|
+
}
|
|
11987
|
+
if (this.#isComponentElement(element) && this.#modifiers.has('lazy')) {
|
|
11988
|
+
console.warn(`[ichigo] <${element.tagName.toLowerCase()}>: the .lazy modifier has no effect on components (the component decides when to emit 'update:${this.#propName}')`);
|
|
11503
11989
|
}
|
|
11504
11990
|
// Parse the expression and create the evaluator
|
|
11505
11991
|
const expression = context.attribute.value;
|
|
@@ -11671,6 +12157,14 @@ class VModelDirective {
|
|
|
11671
12157
|
}
|
|
11672
12158
|
// Evaluate the expression to get the value
|
|
11673
12159
|
const value = this.#evaluator.evaluate();
|
|
12160
|
+
// Components: deliver the value as a property (prop `modelValue`, or the
|
|
12161
|
+
// directive argument). For ichigo components this reaches the reactive
|
|
12162
|
+
// bindings through the generated prop setter.
|
|
12163
|
+
if (this.#isComponentElement(element)) {
|
|
12164
|
+
const propName = this.#resolveComponentPropName(element);
|
|
12165
|
+
element[propName] = value;
|
|
12166
|
+
return;
|
|
12167
|
+
}
|
|
11674
12168
|
// Update the element based on its type
|
|
11675
12169
|
if (element instanceof HTMLInputElement) {
|
|
11676
12170
|
if (element.type === 'checkbox') {
|
|
@@ -11704,6 +12198,19 @@ class VModelDirective {
|
|
|
11704
12198
|
const element = this.#vNode.node;
|
|
11705
12199
|
const eventName = this.#getEventName();
|
|
11706
12200
|
this.#listener = (event) => {
|
|
12201
|
+
// Components: the payload of the `update:<prop>` CustomEvent is the
|
|
12202
|
+
// new value. Only events emitted by this component itself are
|
|
12203
|
+
// accepted — the default $emit fires on the component's root element
|
|
12204
|
+
// (a direct child of the host), so an event whose target lies deeper
|
|
12205
|
+
// was emitted by a nested component and bubbled through.
|
|
12206
|
+
if (this.#isComponentElement(element)) {
|
|
12207
|
+
const src = event.target;
|
|
12208
|
+
if (src !== element && src?.parentElement !== element) {
|
|
12209
|
+
return;
|
|
12210
|
+
}
|
|
12211
|
+
this.#updateBinding(this.#applyModifiers(event.detail));
|
|
12212
|
+
return;
|
|
12213
|
+
}
|
|
11707
12214
|
const target = event.target;
|
|
11708
12215
|
let newValue;
|
|
11709
12216
|
// Get the new value based on element type
|
|
@@ -11858,11 +12365,52 @@ class VModelDirective {
|
|
|
11858
12365
|
}
|
|
11859
12366
|
return result;
|
|
11860
12367
|
}
|
|
12368
|
+
/**
|
|
12369
|
+
* The target prop name on a component: the camelized directive argument,
|
|
12370
|
+
* or the `modelValue` convention when no argument is given.
|
|
12371
|
+
*/
|
|
12372
|
+
get #propName() {
|
|
12373
|
+
return this.#arg ?? 'modelValue';
|
|
12374
|
+
}
|
|
12375
|
+
/**
|
|
12376
|
+
* Returns true when the bound element is a component (custom element).
|
|
12377
|
+
* The `modelValue` / `update:modelValue` contract works for any custom
|
|
12378
|
+
* element that follows it, not only ichigo components.
|
|
12379
|
+
*/
|
|
12380
|
+
#isComponentElement(element) {
|
|
12381
|
+
return element.tagName.includes('-');
|
|
12382
|
+
}
|
|
12383
|
+
/**
|
|
12384
|
+
* Resolves the property name to assign on a component. For ichigo
|
|
12385
|
+
* components the declared prop list fixes up case-insensitive mismatches
|
|
12386
|
+
* (HTML-lowercased arguments vs. camelCase props) and an undeclared target
|
|
12387
|
+
* prop logs a one-time development warning.
|
|
12388
|
+
*/
|
|
12389
|
+
#resolveComponentPropName(element) {
|
|
12390
|
+
const name = this.#propName;
|
|
12391
|
+
const props = element.constructor._props;
|
|
12392
|
+
if (Array.isArray(props)) {
|
|
12393
|
+
const lower = name.toLowerCase();
|
|
12394
|
+
const canonical = props.find(p => p.toLowerCase() === lower);
|
|
12395
|
+
if (canonical) {
|
|
12396
|
+
return canonical;
|
|
12397
|
+
}
|
|
12398
|
+
if (!this.#warnedUndeclaredProp) {
|
|
12399
|
+
this.#warnedUndeclaredProp = true;
|
|
12400
|
+
console.warn(`[ichigo] <${element.tagName.toLowerCase()}>: v-model targets prop '${name}', which is not declared in the component's props`);
|
|
12401
|
+
}
|
|
12402
|
+
}
|
|
12403
|
+
return name;
|
|
12404
|
+
}
|
|
11861
12405
|
/**
|
|
11862
12406
|
* Gets the appropriate event name for the element type.
|
|
11863
12407
|
*/
|
|
11864
12408
|
#getEventName() {
|
|
11865
12409
|
const element = this.#vNode.node;
|
|
12410
|
+
// Components: listen for the `update:<prop>` convention event.
|
|
12411
|
+
if (this.#isComponentElement(element)) {
|
|
12412
|
+
return 'update:' + this.#propName;
|
|
12413
|
+
}
|
|
11866
12414
|
// .lazy modifier: use 'change' event instead of 'input'
|
|
11867
12415
|
if (this.#modifiers.has('lazy')) {
|
|
11868
12416
|
return 'change';
|
|
@@ -13052,8 +13600,7 @@ class VTextDirective {
|
|
|
13052
13600
|
},
|
|
13053
13601
|
applyToDOM() {
|
|
13054
13602
|
const element = vNode.node;
|
|
13055
|
-
|
|
13056
|
-
element.textContent = textContent ?? '';
|
|
13603
|
+
element.textContent = toDisplayString(evaluator.evaluate());
|
|
13057
13604
|
}
|
|
13058
13605
|
};
|
|
13059
13606
|
return updater;
|
|
@@ -13603,9 +14150,10 @@ class VStandardDirectiveParser {
|
|
|
13603
14150
|
// v-bind:<attribute>, :<attribute>
|
|
13604
14151
|
context.attribute.name.startsWith(StandardDirectiveName.V_BIND + ":") ||
|
|
13605
14152
|
context.attribute.name.startsWith(":") ||
|
|
13606
|
-
// v-model, v-model.<modifier>
|
|
14153
|
+
// v-model, v-model.<modifier>, v-model:<arg>, v-model:<arg>.<modifier>
|
|
13607
14154
|
context.attribute.name === StandardDirectiveName.V_MODEL ||
|
|
13608
14155
|
context.attribute.name.startsWith(StandardDirectiveName.V_MODEL + ".") ||
|
|
14156
|
+
context.attribute.name.startsWith(StandardDirectiveName.V_MODEL + ":") ||
|
|
13609
14157
|
// v-resize
|
|
13610
14158
|
context.attribute.name === StandardDirectiveName.V_RESIZE ||
|
|
13611
14159
|
// v-intersection
|
|
@@ -13673,9 +14221,10 @@ class VStandardDirectiveParser {
|
|
|
13673
14221
|
context.attribute.name.startsWith(":")) {
|
|
13674
14222
|
return new VBindDirective(context);
|
|
13675
14223
|
}
|
|
13676
|
-
// v-model, v-model.<modifier>
|
|
14224
|
+
// v-model, v-model.<modifier>, v-model:<arg>, v-model:<arg>.<modifier>
|
|
13677
14225
|
if (context.attribute.name === StandardDirectiveName.V_MODEL ||
|
|
13678
|
-
context.attribute.name.startsWith(StandardDirectiveName.V_MODEL + ".")
|
|
14226
|
+
context.attribute.name.startsWith(StandardDirectiveName.V_MODEL + ".") ||
|
|
14227
|
+
context.attribute.name.startsWith(StandardDirectiveName.V_MODEL + ":")) {
|
|
13679
14228
|
return new VModelDirective(context);
|
|
13680
14229
|
}
|
|
13681
14230
|
// v-resize
|
|
@@ -14099,6 +14648,14 @@ class VApplication {
|
|
|
14099
14648
|
* Flag to indicate if an update is already scheduled.
|
|
14100
14649
|
*/
|
|
14101
14650
|
#updateScheduled = false;
|
|
14651
|
+
/**
|
|
14652
|
+
* The custom element hosting this application when it powers a component
|
|
14653
|
+
* defined via defineComponent(). Set by IchigoElement before mounting.
|
|
14654
|
+
* Used by component-boundary features (e.g. scoped slot outlets) to reach
|
|
14655
|
+
* the host's captured slot templates and parent application scope.
|
|
14656
|
+
* Undefined for regular (non-component) applications.
|
|
14657
|
+
*/
|
|
14658
|
+
#hostElement;
|
|
14102
14659
|
/**
|
|
14103
14660
|
* Creates an instance of the virtual application.
|
|
14104
14661
|
* @param args The initialization arguments for the application.
|
|
@@ -14202,9 +14759,32 @@ class VApplication {
|
|
|
14202
14759
|
get refs() {
|
|
14203
14760
|
return this.#refs;
|
|
14204
14761
|
}
|
|
14762
|
+
/**
|
|
14763
|
+
* The custom element hosting this application when it powers a component
|
|
14764
|
+
* (see the #hostElement field). Undefined for regular applications.
|
|
14765
|
+
*/
|
|
14766
|
+
get hostElement() {
|
|
14767
|
+
return this.#hostElement;
|
|
14768
|
+
}
|
|
14769
|
+
set hostElement(element) {
|
|
14770
|
+
this.#hostElement = element;
|
|
14771
|
+
}
|
|
14772
|
+
/**
|
|
14773
|
+
* Creates a VNode. Directives that need to build VNode trees at runtime
|
|
14774
|
+
* (e.g. the scoped slot outlet) call this instead of constructing VNode
|
|
14775
|
+
* directly, so they can keep their VNode import type-only and avoid a
|
|
14776
|
+
* VNode -> VDirectiveManager -> directive -> VNode module cycle.
|
|
14777
|
+
* @param init The initialization arguments for the virtual node.
|
|
14778
|
+
* @returns The created VNode.
|
|
14779
|
+
*/
|
|
14780
|
+
createVNode(init) {
|
|
14781
|
+
return new VNode(init);
|
|
14782
|
+
}
|
|
14205
14783
|
/**
|
|
14206
14784
|
* Mounts the application.
|
|
14207
14785
|
* @param target The CSS selector string or HTMLElement to mount the application to.
|
|
14786
|
+
* @returns This application instance, so `VDOM.createApp(...).mount('#app')`
|
|
14787
|
+
* can be assigned for later access (e.g. `app.bindings`, `app.unmount()`).
|
|
14208
14788
|
*/
|
|
14209
14789
|
mount(target) {
|
|
14210
14790
|
let element;
|
|
@@ -14235,6 +14815,7 @@ class VApplication {
|
|
|
14235
14815
|
});
|
|
14236
14816
|
element.removeAttribute('v-cloak');
|
|
14237
14817
|
this.#logger.info('Application mounted.');
|
|
14818
|
+
return this;
|
|
14238
14819
|
}
|
|
14239
14820
|
/**
|
|
14240
14821
|
* Unmounts the application and cleans up resources.
|
|
@@ -14285,7 +14866,15 @@ class VApplication {
|
|
|
14285
14866
|
else {
|
|
14286
14867
|
buffer = null;
|
|
14287
14868
|
if (node.nodeType === Node.ELEMENT_NODE) {
|
|
14288
|
-
|
|
14869
|
+
const child = node;
|
|
14870
|
+
// Do not clean inside an expanded component: its DOM belongs
|
|
14871
|
+
// to the component's own application, and removing rendered
|
|
14872
|
+
// (possibly empty) text nodes would detach them from that
|
|
14873
|
+
// application's virtual nodes, freezing its output.
|
|
14874
|
+
if (IchigoElementRegistry.has(child.tagName) && child._ichigoExpanded === true) {
|
|
14875
|
+
continue;
|
|
14876
|
+
}
|
|
14877
|
+
this.#cleanElement(child);
|
|
14289
14878
|
}
|
|
14290
14879
|
}
|
|
14291
14880
|
}
|
|
@@ -14681,7 +15270,8 @@ class VApplication {
|
|
|
14681
15270
|
*/
|
|
14682
15271
|
#emit(name, detail, options) {
|
|
14683
15272
|
// Documentation/validation only: warn when emitting an event not declared in `emits`.
|
|
14684
|
-
|
|
15273
|
+
// `update:*` events are the component v-model convention and are always allowed.
|
|
15274
|
+
if (this.#options.emits && !name.startsWith('update:') && !this.#options.emits.includes(name)) {
|
|
14685
15275
|
this.#logger.warn(`Event '${name}' is emitted but not declared in the 'emits' option.`);
|
|
14686
15276
|
}
|
|
14687
15277
|
const target = options?.target ?? this.#vNode?.node;
|
|
@@ -14784,13 +15374,19 @@ class VDOM {
|
|
|
14784
15374
|
* Mount timing:
|
|
14785
15375
|
* - If the component declares no props, the VApplication is mounted synchronously
|
|
14786
15376
|
* at the end of connectedCallback (the template is known, no props to wait for).
|
|
14787
|
-
* - If the component declares props,
|
|
14788
|
-
*
|
|
14789
|
-
*
|
|
14790
|
-
*
|
|
15377
|
+
* - If the component declares props, a mount microtask is scheduled at the end of
|
|
15378
|
+
* connectedCallback. Prop values delivered synchronously by the parent framework
|
|
15379
|
+
* (VBindDirective runs within the parent's mount task) are stored before the
|
|
15380
|
+
* microtask fires, so data() can read them. A component used without any prop
|
|
15381
|
+
* binding still mounts — with its declared defaults — instead of waiting forever.
|
|
15382
|
+
*
|
|
15383
|
+
* Slot content is captured on the first connection and cached, so a component
|
|
15384
|
+
* that is moved in the DOM (e.g. reordered by v-for) re-expands its template
|
|
15385
|
+
* and redistributes the same slot nodes on reconnection.
|
|
14791
15386
|
*
|
|
14792
|
-
* Subclasses must set the static fields _template, _props and
|
|
14793
|
-
* calling customElements.define(). defineComponent()
|
|
15387
|
+
* Subclasses must set the static fields _template, _props, _propOptions and
|
|
15388
|
+
* _buildOptions before calling customElements.define(). defineComponent()
|
|
15389
|
+
* handles this automatically.
|
|
14794
15390
|
*/
|
|
14795
15391
|
class IchigoElement extends HTMLElement {
|
|
14796
15392
|
/**
|
|
@@ -14810,43 +15406,118 @@ class IchigoElement extends HTMLElement {
|
|
|
14810
15406
|
* Whether a mount microtask is already queued to avoid double-mounting.
|
|
14811
15407
|
*/
|
|
14812
15408
|
#mountScheduled = false;
|
|
15409
|
+
/**
|
|
15410
|
+
* Slot content captured on the first connection. Reused on reconnection
|
|
15411
|
+
* (v-for reorder moves the host element, which re-runs connectedCallback
|
|
15412
|
+
* with the previously expanded template as children — not authored slot
|
|
15413
|
+
* content — so the original capture must be redistributed instead).
|
|
15414
|
+
* `scopedSlots` holds `<template v-slot:...>` children by slot name; they
|
|
15415
|
+
* are rendered per outlet by VSlotOutletDirective, not distributed here.
|
|
15416
|
+
*/
|
|
15417
|
+
#slotCache;
|
|
15418
|
+
/**
|
|
15419
|
+
* True once this element's children are the expanded component template.
|
|
15420
|
+
* Read by VNode during compilation: a parent application must not compile
|
|
15421
|
+
* an expanded component's internals as its own template (the internals
|
|
15422
|
+
* belong to this component's VApplication and scope).
|
|
15423
|
+
*/
|
|
15424
|
+
_ichigoExpanded = false;
|
|
15425
|
+
/**
|
|
15426
|
+
* Host context installed by the parent application's VNode when it
|
|
15427
|
+
* compiles this element. Gives component-boundary features (e.g. scoped
|
|
15428
|
+
* slot outlets) access to the parent scope. Undefined when the element is
|
|
15429
|
+
* placed outside any ichigo application.
|
|
15430
|
+
*/
|
|
15431
|
+
_ichigoHost;
|
|
15432
|
+
/**
|
|
15433
|
+
* The scoped slot templates captured from this element's children, keyed
|
|
15434
|
+
* by slot name ('default' for `v-slot` without a name). Read by
|
|
15435
|
+
* VSlotOutletDirective when the component template renders a `<slot>`.
|
|
15436
|
+
*/
|
|
15437
|
+
get _ichigoScopedSlots() {
|
|
15438
|
+
return this.#slotCache?.scopedSlots;
|
|
15439
|
+
}
|
|
14813
15440
|
connectedCallback() {
|
|
14814
|
-
// ---
|
|
15441
|
+
// --- 0a. Guard: do not expand a template-source element ---
|
|
15442
|
+
// An element still carrying a templatizing directive attribute (v-if
|
|
15443
|
+
// family / v-for) is a template source: the owning application clones it
|
|
15444
|
+
// and inserts the clones (whose directive attributes are stripped during
|
|
15445
|
+
// clone compilation, so the clones expand normally). Expanding the source
|
|
15446
|
+
// here would bake the expansion into every clone, and the clone's
|
|
15447
|
+
// internals would then be compiled in the parent scope.
|
|
15448
|
+
if (this.hasAttribute(StandardDirectiveName.V_IF) ||
|
|
15449
|
+
this.hasAttribute(StandardDirectiveName.V_ELSE_IF) ||
|
|
15450
|
+
this.hasAttribute(StandardDirectiveName.V_ELSE) ||
|
|
15451
|
+
this.hasAttribute(StandardDirectiveName.V_FOR)) {
|
|
15452
|
+
return;
|
|
15453
|
+
}
|
|
15454
|
+
// --- 0b. Reclaim prop values assigned before upgrade ---
|
|
15455
|
+
// A plain property set on the instance (e.g. by a binding delivered
|
|
15456
|
+
// while the element was detached or not yet upgraded) is an own data
|
|
15457
|
+
// property that shadows the prototype accessor generated by
|
|
15458
|
+
// defineComponent — later assignments would keep writing to it and
|
|
15459
|
+
// never reach _setProp. Delete the own property and replay the value
|
|
15460
|
+
// through the accessor.
|
|
15461
|
+
const ctor = this.constructor;
|
|
15462
|
+
for (const prop of ctor._props) {
|
|
15463
|
+
if (Object.prototype.hasOwnProperty.call(this, prop)) {
|
|
15464
|
+
const value = this[prop];
|
|
15465
|
+
delete this[prop];
|
|
15466
|
+
this[prop] = value;
|
|
15467
|
+
}
|
|
15468
|
+
}
|
|
15469
|
+
// --- 0c. Guard: skip if template not yet loaded ---
|
|
14815
15470
|
// customElements.define() may run before loadComponent() completes (e.g. when
|
|
14816
15471
|
// dynamic imports are inlined by the bundler). The static placeholder element
|
|
14817
15472
|
// in the HTML will be removed and replaced by v-for once the template is ready,
|
|
14818
15473
|
// so it is safe to do nothing here.
|
|
14819
|
-
const ctor = this.constructor;
|
|
14820
15474
|
const templateEl = document.querySelector(ctor._template);
|
|
14821
15475
|
if (!templateEl || !(templateEl instanceof HTMLTemplateElement)) {
|
|
14822
15476
|
return;
|
|
14823
15477
|
}
|
|
14824
|
-
// --- 1. Capture slot content
|
|
14825
|
-
|
|
14826
|
-
|
|
14827
|
-
|
|
14828
|
-
|
|
14829
|
-
|
|
14830
|
-
|
|
14831
|
-
|
|
14832
|
-
|
|
14833
|
-
|
|
15478
|
+
// --- 1. Capture slot content (first connection only) ---
|
|
15479
|
+
if (!this.#slotCache) {
|
|
15480
|
+
const defaultNodes = [];
|
|
15481
|
+
const namedNodes = new Map();
|
|
15482
|
+
const scopedSlots = new Map();
|
|
15483
|
+
for (const child of Array.from(this.childNodes)) {
|
|
15484
|
+
if (child.nodeType === Node.ELEMENT_NODE) {
|
|
15485
|
+
const el = child;
|
|
15486
|
+
// <template v-slot:name="scopeVar"> — a scoped slot template.
|
|
15487
|
+
// Captured for per-outlet rendering (VSlotOutletDirective),
|
|
15488
|
+
// not distributed statically.
|
|
15489
|
+
if (el instanceof HTMLTemplateElement) {
|
|
15490
|
+
const scoped = this.#parseVSlotAttribute(el);
|
|
15491
|
+
if (scoped) {
|
|
15492
|
+
scopedSlots.set(scoped.name, { template: el, scopeVar: scoped.scopeVar });
|
|
15493
|
+
continue;
|
|
15494
|
+
}
|
|
15495
|
+
}
|
|
15496
|
+
const slotName = el.getAttribute('slot');
|
|
15497
|
+
if (slotName) {
|
|
15498
|
+
if (!namedNodes.has(slotName)) {
|
|
15499
|
+
namedNodes.set(slotName, []);
|
|
15500
|
+
}
|
|
15501
|
+
namedNodes.get(slotName).push(el);
|
|
15502
|
+
// Remove slot attribute so ichigo.js doesn't try to bind it
|
|
15503
|
+
el.removeAttribute('slot');
|
|
15504
|
+
}
|
|
15505
|
+
else {
|
|
15506
|
+
defaultNodes.push(el);
|
|
14834
15507
|
}
|
|
14835
|
-
namedSlotNodes.get(slotName).push(el);
|
|
14836
|
-
// Remove slot attribute so ichigo.js doesn't try to bind it
|
|
14837
|
-
el.removeAttribute('slot');
|
|
14838
|
-
}
|
|
14839
|
-
else {
|
|
14840
|
-
defaultSlotNodes.push(el);
|
|
14841
15508
|
}
|
|
14842
|
-
|
|
14843
|
-
|
|
14844
|
-
|
|
14845
|
-
|
|
15509
|
+
else if (child.nodeType === Node.TEXT_NODE) {
|
|
15510
|
+
if ((child.textContent ?? '').trim()) {
|
|
15511
|
+
defaultNodes.push(child);
|
|
15512
|
+
}
|
|
14846
15513
|
}
|
|
14847
15514
|
}
|
|
15515
|
+
this.#slotCache = { defaultNodes, namedNodes, scopedSlots };
|
|
14848
15516
|
}
|
|
14849
|
-
|
|
15517
|
+
const { defaultNodes, namedNodes } = this.#slotCache;
|
|
15518
|
+
// Clear host element so we can append the cloned template.
|
|
15519
|
+
// (On reconnection this discards the previous expansion; the cached slot
|
|
15520
|
+
// nodes are still referenced and are moved into the fresh clone below.)
|
|
14850
15521
|
while (this.firstChild) {
|
|
14851
15522
|
this.removeChild(this.firstChild);
|
|
14852
15523
|
}
|
|
@@ -14854,7 +15525,7 @@ class IchigoElement extends HTMLElement {
|
|
|
14854
15525
|
const fragment = templateEl.content.cloneNode(true);
|
|
14855
15526
|
const root = this.#findRootElement(fragment);
|
|
14856
15527
|
// --- 3. Distribute named slot content ---
|
|
14857
|
-
for (const [name, nodes] of
|
|
15528
|
+
for (const [name, nodes] of namedNodes) {
|
|
14858
15529
|
const slot = root.querySelector(`slot[name="${name}"]`);
|
|
14859
15530
|
if (slot) {
|
|
14860
15531
|
slot.replaceWith(...nodes);
|
|
@@ -14862,22 +15533,25 @@ class IchigoElement extends HTMLElement {
|
|
|
14862
15533
|
}
|
|
14863
15534
|
// --- 4. Distribute default slot content ---
|
|
14864
15535
|
const defaultSlot = root.querySelector('slot:not([name])');
|
|
14865
|
-
if (defaultSlot &&
|
|
14866
|
-
defaultSlot.replaceWith(...
|
|
15536
|
+
if (defaultSlot && defaultNodes.length > 0) {
|
|
15537
|
+
defaultSlot.replaceWith(...defaultNodes);
|
|
14867
15538
|
}
|
|
14868
15539
|
// Attach the populated template to the host element
|
|
14869
15540
|
this.appendChild(root);
|
|
14870
15541
|
this.#mountRoot = root;
|
|
14871
|
-
|
|
14872
|
-
this.#scheduleMountIfNeeded();
|
|
15542
|
+
this._ichigoExpanded = true;
|
|
14873
15543
|
// --- 5. Mount strategy ---
|
|
14874
|
-
//
|
|
14875
|
-
//
|
|
14876
|
-
//
|
|
15544
|
+
// No declared props: mount immediately (nothing to wait for).
|
|
15545
|
+
// Declared props: schedule a mount microtask unconditionally. Prop values
|
|
15546
|
+
// pushed synchronously by the parent (VBindDirective) land before the
|
|
15547
|
+
// microtask runs; a component used without any binding still mounts,
|
|
15548
|
+
// with its declared defaults.
|
|
14877
15549
|
if (ctor._props.length === 0) {
|
|
14878
15550
|
this.#doMount();
|
|
14879
15551
|
}
|
|
14880
|
-
|
|
15552
|
+
else {
|
|
15553
|
+
this.#scheduleMountIfNeeded();
|
|
15554
|
+
}
|
|
14881
15555
|
}
|
|
14882
15556
|
disconnectedCallback() {
|
|
14883
15557
|
this.#mountRoot = undefined;
|
|
@@ -14891,8 +15565,17 @@ class IchigoElement extends HTMLElement {
|
|
|
14891
15565
|
* Called by the property setters generated by defineComponent().
|
|
14892
15566
|
* Before mount: stores the value and schedules a mount microtask.
|
|
14893
15567
|
* After mount: pushes the value directly into the reactive bindings.
|
|
15568
|
+
* An undefined value is replaced by the prop's declared default, if any.
|
|
14894
15569
|
*/
|
|
14895
15570
|
_setProp(name, value) {
|
|
15571
|
+
const ctor = this.constructor;
|
|
15572
|
+
const opts = ctor._propOptions[name];
|
|
15573
|
+
if (opts) {
|
|
15574
|
+
if (value === undefined) {
|
|
15575
|
+
value = IchigoElement.#resolveDefault(opts);
|
|
15576
|
+
}
|
|
15577
|
+
this.#validateProp(name, value, opts);
|
|
15578
|
+
}
|
|
14896
15579
|
this.#propValues[name] = value;
|
|
14897
15580
|
if (this.#app) {
|
|
14898
15581
|
this.#app.bindings?.set(name, value);
|
|
@@ -14913,9 +15596,16 @@ class IchigoElement extends HTMLElement {
|
|
|
14913
15596
|
*/
|
|
14914
15597
|
static _template;
|
|
14915
15598
|
/**
|
|
14916
|
-
* List of declared prop names. Used to decide whether to defer mounting
|
|
15599
|
+
* List of declared prop names. Used to decide whether to defer mounting,
|
|
15600
|
+
* and by VBindDirective to resolve kebab-case attributes to prop names.
|
|
14917
15601
|
*/
|
|
14918
15602
|
static _props = [];
|
|
15603
|
+
/**
|
|
15604
|
+
* Per-prop declaration options (type / default / required / validator),
|
|
15605
|
+
* keyed by prop name. Normalized by defineComponent(): a prop declared
|
|
15606
|
+
* without options gets an empty object.
|
|
15607
|
+
*/
|
|
15608
|
+
static _propOptions = {};
|
|
14919
15609
|
/**
|
|
14920
15610
|
* Factory that builds VApplicationOptions from the current prop values.
|
|
14921
15611
|
* Implemented by defineComponent() as a closure that captures the user's options.
|
|
@@ -14924,7 +15614,8 @@ class IchigoElement extends HTMLElement {
|
|
|
14924
15614
|
// --- Private helpers ---
|
|
14925
15615
|
/**
|
|
14926
15616
|
* Schedules a mount microtask if the DOM root is ready and no mount is pending.
|
|
14927
|
-
* Called from _setProp() so the mount happens after
|
|
15617
|
+
* Called from connectedCallback and _setProp() so the mount happens after
|
|
15618
|
+
* synchronously delivered prop values are stored.
|
|
14928
15619
|
*/
|
|
14929
15620
|
#scheduleMountIfNeeded() {
|
|
14930
15621
|
if (this.#mountScheduled || this.#app || !this.#mountRoot) {
|
|
@@ -14941,10 +15632,85 @@ class IchigoElement extends HTMLElement {
|
|
|
14941
15632
|
return;
|
|
14942
15633
|
}
|
|
14943
15634
|
const ctor = this.constructor;
|
|
15635
|
+
// Attribute fallback: a prop that never received a property value may
|
|
15636
|
+
// have been provided as a plain HTML attribute — static usage like
|
|
15637
|
+
// <my-badge label="x">, or a binding delivered by setAttribute before
|
|
15638
|
+
// the element was upgraded. Attribute values are strings; a declared
|
|
15639
|
+
// Boolean type turns presence ("") into true, and a declared Number
|
|
15640
|
+
// type parses numeric strings.
|
|
15641
|
+
for (const name of ctor._props) {
|
|
15642
|
+
if (this.#propValues[name] !== undefined) {
|
|
15643
|
+
continue;
|
|
15644
|
+
}
|
|
15645
|
+
const attr = this.getAttribute(IchigoElement.#camelToKebab(name)) ?? this.getAttribute(name.toLowerCase());
|
|
15646
|
+
if (attr === null) {
|
|
15647
|
+
continue;
|
|
15648
|
+
}
|
|
15649
|
+
const opts = ctor._propOptions[name];
|
|
15650
|
+
let value = attr;
|
|
15651
|
+
if (opts && IchigoElement.#typeIncludes(opts.type, Boolean) && attr === '') {
|
|
15652
|
+
value = true;
|
|
15653
|
+
}
|
|
15654
|
+
else if (opts && IchigoElement.#typeIncludes(opts.type, Number) && attr !== '' && !Number.isNaN(Number(attr))) {
|
|
15655
|
+
value = Number(attr);
|
|
15656
|
+
}
|
|
15657
|
+
if (opts) {
|
|
15658
|
+
this.#validateProp(name, value, opts);
|
|
15659
|
+
}
|
|
15660
|
+
this.#propValues[name] = value;
|
|
15661
|
+
}
|
|
15662
|
+
// Resolve defaults for props that never received a value, and report
|
|
15663
|
+
// missing required props (warning only; the mount proceeds either way).
|
|
15664
|
+
for (const [name, opts] of Object.entries(ctor._propOptions)) {
|
|
15665
|
+
if (this.#propValues[name] === undefined) {
|
|
15666
|
+
const resolved = IchigoElement.#resolveDefault(opts);
|
|
15667
|
+
if (resolved !== undefined) {
|
|
15668
|
+
this.#propValues[name] = resolved;
|
|
15669
|
+
}
|
|
15670
|
+
else if (opts.required) {
|
|
15671
|
+
this.#warn(`required prop '${name}' is missing`);
|
|
15672
|
+
}
|
|
15673
|
+
}
|
|
15674
|
+
}
|
|
14944
15675
|
const options = ctor._buildOptions(this.#propValues);
|
|
14945
15676
|
this.#app = VDOM.createApp(options);
|
|
15677
|
+
// Give the application a handle back to this host element so
|
|
15678
|
+
// component-boundary features (e.g. scoped slot outlets) can reach the
|
|
15679
|
+
// captured slot templates and the parent application scope.
|
|
15680
|
+
this.#app.hostElement = this;
|
|
14946
15681
|
this.#app.mount(this.#mountRoot);
|
|
14947
15682
|
}
|
|
15683
|
+
/**
|
|
15684
|
+
* Parses a `v-slot` declaration on a `<template>` child, accepting
|
|
15685
|
+
* `v-slot` / `v-slot:name` and the `#name` shorthand. Returns the slot
|
|
15686
|
+
* name ('default' when omitted) and the scope variable name, or undefined
|
|
15687
|
+
* when the template carries no v-slot attribute. The scope variable must
|
|
15688
|
+
* be a single identifier; an invalid one is ignored with a warning.
|
|
15689
|
+
*/
|
|
15690
|
+
#parseVSlotAttribute(template) {
|
|
15691
|
+
for (const attr of Array.from(template.attributes)) {
|
|
15692
|
+
let name;
|
|
15693
|
+
if (attr.name === 'v-slot') {
|
|
15694
|
+
name = 'default';
|
|
15695
|
+
}
|
|
15696
|
+
else if (attr.name.startsWith('v-slot:')) {
|
|
15697
|
+
name = attr.name.substring(7);
|
|
15698
|
+
}
|
|
15699
|
+
else if (attr.name.startsWith('#')) {
|
|
15700
|
+
name = attr.name.substring(1);
|
|
15701
|
+
}
|
|
15702
|
+
if (name === undefined || name === '') {
|
|
15703
|
+
continue;
|
|
15704
|
+
}
|
|
15705
|
+
let scopeVar = attr.value.trim();
|
|
15706
|
+
if (scopeVar && !/^[A-Za-z_$][\w$]*$/.test(scopeVar)) {
|
|
15707
|
+
this.#warn(`v-slot scope must be a single identifier; '${scopeVar}' is ignored (destructuring is not supported)`);
|
|
15708
|
+
scopeVar = '';
|
|
15709
|
+
}
|
|
15710
|
+
return { name, scopeVar };
|
|
15711
|
+
}
|
|
15712
|
+
return undefined;
|
|
15713
|
+
}
|
|
14948
15714
|
#findRootElement(fragment) {
|
|
14949
15715
|
for (const node of Array.from(fragment.childNodes)) {
|
|
14950
15716
|
if (node.nodeType === Node.ELEMENT_NODE) {
|
|
@@ -14953,6 +15719,74 @@ class IchigoElement extends HTMLElement {
|
|
|
14953
15719
|
}
|
|
14954
15720
|
throw new Error(`IchigoElement: no root element found in template '${this.constructor._template}'`);
|
|
14955
15721
|
}
|
|
15722
|
+
/**
|
|
15723
|
+
* Resolves a prop's default value. A function default is called as a
|
|
15724
|
+
* factory unless the declared type includes Function, in which case the
|
|
15725
|
+
* function itself is the value.
|
|
15726
|
+
*/
|
|
15727
|
+
static #resolveDefault(opts) {
|
|
15728
|
+
const def = opts.default;
|
|
15729
|
+
if (def === undefined) {
|
|
15730
|
+
return undefined;
|
|
15731
|
+
}
|
|
15732
|
+
if (typeof def === 'function' && !IchigoElement.#typeIncludes(opts.type, Function)) {
|
|
15733
|
+
return def();
|
|
15734
|
+
}
|
|
15735
|
+
return def;
|
|
15736
|
+
}
|
|
15737
|
+
static #typeIncludes(type, ctor) {
|
|
15738
|
+
if (!type) {
|
|
15739
|
+
return false;
|
|
15740
|
+
}
|
|
15741
|
+
return Array.isArray(type) ? type.includes(ctor) : type === ctor;
|
|
15742
|
+
}
|
|
15743
|
+
static #camelToKebab(str) {
|
|
15744
|
+
return str.replace(/([A-Z])/g, '-$1').toLowerCase();
|
|
15745
|
+
}
|
|
15746
|
+
/**
|
|
15747
|
+
* Validates a delivered prop value against its declaration.
|
|
15748
|
+
* Warnings only — the value is stored and delivered regardless.
|
|
15749
|
+
* null/undefined values skip both checks (undefined is handled by
|
|
15750
|
+
* default/required; null is an explicit "no value").
|
|
15751
|
+
*/
|
|
15752
|
+
#validateProp(name, value, opts) {
|
|
15753
|
+
if (value === null || value === undefined) {
|
|
15754
|
+
return;
|
|
15755
|
+
}
|
|
15756
|
+
if (opts.type) {
|
|
15757
|
+
const types = Array.isArray(opts.type) ? opts.type : [opts.type];
|
|
15758
|
+
if (!types.some(t => IchigoElement.#matchesType(value, t))) {
|
|
15759
|
+
const expected = types.map(t => t.name ?? String(t)).join(' | ');
|
|
15760
|
+
const actual = typeof value === 'object'
|
|
15761
|
+
? (value?.constructor?.name ?? 'object')
|
|
15762
|
+
: typeof value;
|
|
15763
|
+
this.#warn(`prop '${name}' expects ${expected}, got ${actual}`);
|
|
15764
|
+
}
|
|
15765
|
+
}
|
|
15766
|
+
if (opts.validator && !opts.validator(value)) {
|
|
15767
|
+
this.#warn(`prop '${name}' failed its validator`);
|
|
15768
|
+
}
|
|
15769
|
+
}
|
|
15770
|
+
static #matchesType(value, type) {
|
|
15771
|
+
switch (type) {
|
|
15772
|
+
case String: return typeof value === 'string';
|
|
15773
|
+
case Number: return typeof value === 'number';
|
|
15774
|
+
case Boolean: return typeof value === 'boolean';
|
|
15775
|
+
case Function: return typeof value === 'function';
|
|
15776
|
+
case Array: return Array.isArray(value);
|
|
15777
|
+
case Object: return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
15778
|
+
default:
|
|
15779
|
+
try {
|
|
15780
|
+
return value instanceof type;
|
|
15781
|
+
}
|
|
15782
|
+
catch {
|
|
15783
|
+
return false;
|
|
15784
|
+
}
|
|
15785
|
+
}
|
|
15786
|
+
}
|
|
15787
|
+
#warn(message) {
|
|
15788
|
+
console.warn(`[ichigo] <${this.tagName.toLowerCase()}>: ${message}`);
|
|
15789
|
+
}
|
|
14956
15790
|
}
|
|
14957
15791
|
|
|
14958
15792
|
// Copyright (c) 2025 MintJams Inc. Licensed under MIT License.
|
|
@@ -14973,9 +15807,8 @@ class IchigoElement extends HTMLElement {
|
|
|
14973
15807
|
* ```typescript
|
|
14974
15808
|
* defineComponent('my-list', {
|
|
14975
15809
|
* template: '#my-list',
|
|
14976
|
-
* props:
|
|
14977
|
-
*
|
|
14978
|
-
* return { items: this.items ?? [] };
|
|
15810
|
+
* props: {
|
|
15811
|
+
* items: { type: Array, default: () => [] },
|
|
14979
15812
|
* },
|
|
14980
15813
|
* });
|
|
14981
15814
|
* ```
|
|
@@ -14985,15 +15818,34 @@ class IchigoElement extends HTMLElement {
|
|
|
14985
15818
|
* </my-list>
|
|
14986
15819
|
* ```
|
|
14987
15820
|
*
|
|
15821
|
+
* Props may be declared as a simple name list (`props: ['items']`) or as a
|
|
15822
|
+
* record with per-prop options (type / default / required / validator).
|
|
15823
|
+
* See {@link PropOptions} for the declaration semantics.
|
|
15824
|
+
*
|
|
14988
15825
|
* @param tagName Custom element tag name (must contain a hyphen, e.g. 'my-card').
|
|
14989
15826
|
* @param options Component options including template selector and optional props.
|
|
14990
15827
|
*/
|
|
14991
15828
|
function defineComponent(tagName, options) {
|
|
14992
15829
|
const { props = [], template, data, computed, methods, watch, emits, logLevel } = options;
|
|
15830
|
+
// Normalize the props declaration: a plain name list becomes a record of
|
|
15831
|
+
// empty option objects so the rest of the code handles a single shape.
|
|
15832
|
+
const propOptions = {};
|
|
15833
|
+
if (Array.isArray(props)) {
|
|
15834
|
+
for (const name of props) {
|
|
15835
|
+
propOptions[name] = {};
|
|
15836
|
+
}
|
|
15837
|
+
}
|
|
15838
|
+
else {
|
|
15839
|
+
for (const [name, opts] of Object.entries(props)) {
|
|
15840
|
+
propOptions[name] = opts ?? {};
|
|
15841
|
+
}
|
|
15842
|
+
}
|
|
15843
|
+
const propNames = Object.keys(propOptions);
|
|
14993
15844
|
// Build a subclass of IchigoElement specific to this component
|
|
14994
15845
|
class ComponentElement extends IchigoElement {
|
|
14995
15846
|
static _template = template;
|
|
14996
|
-
static _props =
|
|
15847
|
+
static _props = propNames;
|
|
15848
|
+
static _propOptions = propOptions;
|
|
14997
15849
|
static _buildOptions(propValues) {
|
|
14998
15850
|
return {
|
|
14999
15851
|
data() {
|
|
@@ -15018,7 +15870,7 @@ function defineComponent(tagName, options) {
|
|
|
15018
15870
|
}
|
|
15019
15871
|
// Generate a property getter/setter for each declared prop.
|
|
15020
15872
|
// This enables the parent VApplication to push updates via `element.propName = value`.
|
|
15021
|
-
for (const prop of
|
|
15873
|
+
for (const prop of propNames) {
|
|
15022
15874
|
Object.defineProperty(ComponentElement.prototype, prop, {
|
|
15023
15875
|
get() {
|
|
15024
15876
|
return this._getProp(prop);
|
|
@@ -15031,7 +15883,9 @@ function defineComponent(tagName, options) {
|
|
|
15031
15883
|
});
|
|
15032
15884
|
}
|
|
15033
15885
|
customElements.define(tagName, ComponentElement);
|
|
15886
|
+
// Record the tag so VNode can recognize component boundaries during compilation.
|
|
15887
|
+
IchigoElementRegistry.add(tagName);
|
|
15034
15888
|
}
|
|
15035
15889
|
|
|
15036
|
-
export { ExpressionUtils, IchigoElement, ReactiveProxy, VComponent, VComponentRegistry, VDOM, defineComponent };
|
|
15890
|
+
export { ExpressionUtils, IchigoElement, ReactiveProxy, VComponent, VComponentRegistry, VDOM, defineComponent, toDisplayString };
|
|
15037
15891
|
//# sourceMappingURL=ichigo.esm.js.map
|