@awc-ui/vue 1.0.0-beta.9 → 1.0.1

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/lib/runtime.ts ADDED
@@ -0,0 +1,220 @@
1
+ // Hand-owned Vue adapter. Generated proxies import this through withVueRuntime.
2
+ // @ts-nocheck
3
+ // It's easier and safer for Volar to disable typechecking and let the return type inference do its job.
4
+ import { defineComponent, getCurrentInstance, h, inject, ref, Ref } from 'vue';
5
+
6
+ export interface InputProps<T> {
7
+ modelValue?: T;
8
+ }
9
+
10
+ const UPDATE_VALUE_EVENT = 'update:modelValue';
11
+ const MODEL_VALUE = 'modelValue';
12
+ const ROUTER_LINK_VALUE = 'routerLink';
13
+ const NAV_MANAGER = 'navManager';
14
+ const ROUTER_PROP_PREFIX = 'router';
15
+ const ARIA_PROP_PREFIX = 'aria';
16
+ /**
17
+ * Starting in Vue 3.1.0, all properties are
18
+ * added as keys to the props object, even if
19
+ * they are not being used. In order to correctly
20
+ * account for both value props and v-model props,
21
+ * we need to check if the key exists for Vue <3.1.0
22
+ * and then check if it is not undefined for Vue >= 3.1.0.
23
+ * See https://github.com/vuejs/vue-next/issues/3889
24
+ */
25
+ const EMPTY_PROP = Symbol();
26
+ const DEFAULT_EMPTY_PROP = { default: EMPTY_PROP };
27
+
28
+ interface NavManager<T = any> {
29
+ navigate: (options: T) => void;
30
+ }
31
+
32
+ const getComponentClasses = (classes: unknown) => {
33
+ return (classes as string)?.split(' ') || [];
34
+ };
35
+
36
+ const getElementClasses = (
37
+ ref: Ref<HTMLElement | undefined>,
38
+ componentClasses: Set<string>,
39
+ defaultClasses: string[] = []
40
+ ) => {
41
+ return [...Array.from(ref.value?.classList || []), ...defaultClasses].filter(
42
+ (c: string, i, self) => !componentClasses.has(c) && self.indexOf(c) === i
43
+ );
44
+ };
45
+
46
+ /**
47
+ * Create a callback to define a Vue component wrapper around a Web Component.
48
+ *
49
+ * @prop name - The component tag name (i.e. `ion-button`)
50
+ * @prop componentProps - An array of properties on the
51
+ * component. These usually match up with the @Prop definitions
52
+ * in each component's TSX file.
53
+ * @prop customElement - An option custom element instance to pass
54
+ * to customElements.define. Only set if `includeImportCustomElements: true` in your config.
55
+ * @prop modelProp - The prop that v-model binds to (i.e. value)
56
+ * @prop modelUpdateEvent - The event that is fired from your Web Component when the value changes (i.e. ionChange)
57
+ */
58
+ export const defineContainer = <Props, VModelType = string | number | boolean>(
59
+ name: string,
60
+ defineCustomElement: any,
61
+ componentProps: string[] = [],
62
+ modelProp?: string,
63
+ modelUpdateEvent?: string
64
+ ) => {
65
+ /**
66
+ * Create a Vue component wrapper around a Web Component.
67
+ * Note: The `props` here are not all properties on a component.
68
+ * They refer to whatever properties are set on an instance of a component.
69
+ */
70
+
71
+ if (defineCustomElement !== undefined) {
72
+ defineCustomElement();
73
+ }
74
+
75
+ // Stencil includes both props and events in this array. AWC custom events
76
+ // consistently use the mdXxx prefix; ordinary component props do not.
77
+ const eventNames = componentProps.filter((name) => /^md[A-Z]/.test(name));
78
+ const propertyNames = componentProps.filter((name) => !eventNames.includes(name));
79
+
80
+ const Container = defineComponent<Props & InputProps<VModelType>>((props, { attrs, slots, emit }) => {
81
+ let modelPropValue = props[modelProp];
82
+ const containerRef = ref<HTMLElement>();
83
+ const classes = new Set(getComponentClasses(attrs.class));
84
+
85
+ const currentInstance = getCurrentInstance();
86
+ const hasRouter = currentInstance?.appContext?.provides[NAV_MANAGER];
87
+ const navManager: NavManager | undefined = hasRouter ? inject(NAV_MANAGER) : undefined;
88
+ const handleRouterLink = (ev: Event) => {
89
+ const { routerLink } = props;
90
+ if (routerLink === EMPTY_PROP) return;
91
+
92
+ if (navManager !== undefined) {
93
+ /**
94
+ * This prevents the browser from
95
+ * performing a page reload when pressing
96
+ * an Ionic component with routerLink.
97
+ * The page reload interferes with routing
98
+ * and causes ion-back-button to disappear
99
+ * since the local history is wiped on reload.
100
+ */
101
+ ev.preventDefault();
102
+
103
+ let navigationPayload: any = { event: ev };
104
+ for (const key in props) {
105
+ const value = props[key];
106
+ if (props.hasOwnProperty(key) && key.startsWith(ROUTER_PROP_PREFIX) && value !== EMPTY_PROP) {
107
+ navigationPayload[key] = value;
108
+ }
109
+ }
110
+
111
+ navManager.navigate(navigationPayload);
112
+ } else {
113
+ console.warn('Tried to navigate, but no router was found. Make sure you have mounted Vue Router.');
114
+ }
115
+ };
116
+
117
+ return () => {
118
+ modelPropValue = props[modelProp];
119
+
120
+ getComponentClasses(attrs.class).forEach((value) => {
121
+ classes.add(value);
122
+ });
123
+
124
+ const oldClick = props.onClick;
125
+ const handleClick = (ev: Event) => {
126
+ if (oldClick !== undefined) {
127
+ oldClick(ev);
128
+ }
129
+ if (!ev.defaultPrevented) {
130
+ handleRouterLink(ev);
131
+ }
132
+ };
133
+
134
+ let propsToAdd: any = {
135
+ ref: containerRef,
136
+ class: getElementClasses(containerRef, classes),
137
+ onClick: handleClick,
138
+ };
139
+
140
+ /**
141
+ * We can use Object.entries here
142
+ * to avoid the hasOwnProperty check,
143
+ * but that would require 2 iterations
144
+ * where as this only requires 1.
145
+ */
146
+ for (const key in props) {
147
+ const value = props[key];
148
+ if ((props.hasOwnProperty(key) && value !== EMPTY_PROP) || key.startsWith(ARIA_PROP_PREFIX)) {
149
+ propsToAdd[key] = value;
150
+ }
151
+ }
152
+
153
+ if (modelProp) {
154
+ /**
155
+ * If form value property was set using v-model
156
+ * then we should use that value.
157
+ * Otherwise, check to see if form value property
158
+ * was set as a static value (i.e. no v-model).
159
+ */
160
+ if (props[MODEL_VALUE] !== EMPTY_PROP) {
161
+ propsToAdd = {
162
+ ...propsToAdd,
163
+ ...(name === 'md-radio'
164
+ ? { checked: props[MODEL_VALUE] != null && Object.is(props[MODEL_VALUE], props.value) }
165
+ : { [modelProp]: props[MODEL_VALUE] }),
166
+ };
167
+ } else if (modelPropValue !== EMPTY_PROP) {
168
+ propsToAdd = {
169
+ ...propsToAdd,
170
+ [modelProp]: modelPropValue,
171
+ };
172
+ }
173
+ }
174
+
175
+ // If router link is defined, add href to props
176
+ // in order to properly render an anchor tag inside
177
+ // of components that should become activatable and
178
+ // focusable with router link.
179
+ if (props[ROUTER_LINK_VALUE] !== EMPTY_PROP) {
180
+ propsToAdd = {
181
+ ...propsToAdd,
182
+ href: props[ROUTER_LINK_VALUE],
183
+ };
184
+ }
185
+
186
+ for (const eventName of eventNames) {
187
+ // Vue's onMdInput DOM fallback listens for md-input. The on: prefix
188
+ // preserves the actual case-sensitive CustomEvent name instead.
189
+ propsToAdd[`on:${eventName}`] = (event: Event) => {
190
+ if (modelProp && eventName === modelUpdateEvent && event.target === event.currentTarget && (name !== 'md-radio' || (event.target as any).checked)) {
191
+ emit(UPDATE_VALUE_EVENT, (event.target as any)[modelProp]);
192
+ }
193
+ emit(eventName, event);
194
+ };
195
+ }
196
+ return h(name, propsToAdd, slots.default && slots.default());
197
+ };
198
+ });
199
+
200
+ if (typeof Container !== 'function') {
201
+ Container.name = name;
202
+
203
+ Container.props = {
204
+ [ROUTER_LINK_VALUE]: DEFAULT_EMPTY_PROP,
205
+ };
206
+
207
+ propertyNames.forEach((componentProp) => {
208
+ Container.props[componentProp] = DEFAULT_EMPTY_PROP;
209
+ });
210
+
211
+ Container.emits = [...eventNames, ...(modelProp ? [UPDATE_VALUE_EVENT] : [])];
212
+
213
+ if (modelProp) {
214
+ Container.props[MODEL_VALUE] = DEFAULT_EMPTY_PROP;
215
+
216
+ }
217
+ }
218
+
219
+ return Container;
220
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@awc-ui/vue",
3
- "version": "1.0.0-beta.9",
3
+ "version": "1.0.1",
4
4
  "description": "Typed Vue 3 components for accessible Material Design 3 Web Components, with automatic registration and Nuxt SSR support",
5
5
  "keywords": [
6
6
  "vue",
@@ -55,12 +55,12 @@
55
55
  ],
56
56
  "peerDependencies": {
57
57
  "vue": ">=3",
58
- "@awc-ui/core": "1.0.0-beta.9"
58
+ "@awc-ui/core": "1.0.1"
59
59
  },
60
60
  "devDependencies": {
61
61
  "esbuild": "^0.25.12",
62
62
  "typescript": "^5.4.0",
63
- "@awc-ui/core": "1.0.0-beta.9"
63
+ "@awc-ui/core": "1.0.1"
64
64
  },
65
65
  "license": "MIT",
66
66
  "author": "AWC UI",
@@ -74,6 +74,7 @@
74
74
  "url": "https://github.com/awc-ui/core/issues"
75
75
  },
76
76
  "scripts": {
77
- "build": "rm -rf dist && tsc -p tsconfig.json && esbuild lib/index.ts --bundle --format=esm --target=es2020 --sourcemap --outfile=dist/index.mjs --external:vue --external:@awc-ui/core --external:@awc-ui/core/*"
77
+ "build": "rm -rf dist && tsc -p tsconfig.json && esbuild lib/index.ts --bundle --format=esm --target=es2020 --sourcemap --outfile=dist/index.mjs --external:vue --external:@awc-ui/core --external:@awc-ui/core/*",
78
+ "test:integration": "node tests/generation.mjs && node tests/forms.mjs"
78
79
  }
79
80
  }