diff --git a/templates/vue3/client/js/components/Hello.vue b/templates/vue3/client/js/components/Hello.vue index caaa6877..3ecd5f79 100644 --- a/templates/vue3/client/js/components/Hello.vue +++ b/templates/vue3/client/js/components/Hello.vue @@ -13,7 +13,7 @@ import { computed, defineComponent, ref } from 'vue'; export default defineComponent({ props: { - language: { + sdk: { type: String, required: true }, @@ -36,7 +36,7 @@ export default defineComponent({ }; // https://v3.vuejs.org/guide/composition-api-introduction.html#standalone-computed-properties - const message = computed(() => `Hello from ${props.language} and ${props.compiler}!`); + const message = computed(() => `Hello from ${props.sdk} and ${props.compiler}!`); // Everything that are returned will be exposed to the component template! return { diff --git a/templates/vue3/client/js/icons.ts b/templates/vue3/client/js/icons.ts new file mode 100644 index 00000000..912dd315 --- /dev/null +++ b/templates/vue3/client/js/icons.ts @@ -0,0 +1,8 @@ +import { library } from '@fortawesome/fontawesome-svg-core'; +import { faChevronUp, faCheck } from '@fortawesome/free-solid-svg-icons'; +import { } from '@fortawesome/free-regular-svg-icons'; + +library.add(faChevronUp, faCheck); + +// Reduce dll size by only importing icons which are actually being used: +// https://fontawesome.com/how-to-use/use-with-node-js diff --git a/templates/vue3/client/js/index.ts b/templates/vue3/client/js/index.ts index 93a2765f..152796f6 100644 --- a/templates/vue3/client/js/index.ts +++ b/templates/vue3/client/js/index.ts @@ -8,4 +8,5 @@ import 'ts-polyfill/lib/es2019-object'; import 'ts-polyfill/lib/es2019-string'; import 'ts-polyfill/lib/es2020-string'; +import './icons'; import './vue-project'; diff --git a/templates/vue3/client/js/vue-project.ts b/templates/vue3/client/js/vue-project.ts index acd0236d..ab64439a 100644 --- a/templates/vue3/client/js/vue-project.ts +++ b/templates/vue3/client/js/vue-project.ts @@ -1,28 +1,29 @@ -import * as Vue from 'vue'; +import Vue from 'vue'; +import { renderAsyncComponent } from './vue-renderer'; -type VueAsync = () => Promise; +import { defineRule, Form, Field } from 'vee-validate'; +import AllRules from '@vee-validate/rules'; +import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'; -function renderAsyncComponent(tag: string, factory: VueAsync) { - const ac = Vue.defineAsyncComponent(factory); - const elements = document.getElementsByTagName(tag); - - for (const e of elements) { - const props: Record = {}; - // enable passing HTML attributes as component props - if (e.hasAttributes()) { - for (const attr of e.attributes) { - if (attr.name.startsWith(':')) { - props[attr.name.substr(1)] = JSON.parse(attr.value); - } else { - // simple string - props[attr.name] = attr.value; - } - } - } +// define validation rules +Object.keys(AllRules).forEach(rule => { + defineRule(rule, AllRules[rule]); +}); - const app = Vue.createApp(ac, props); - app.mount(e); - } +/** + * allows declaring components that can be used in all components. + * usually these components are third-party libraries + * or any first-party reusable components such as a custom submit button. + * @param app + */ +function configure(app: Vue.App) { + // https://v3.vuejs.org/style-guide/#component-name-casing-in-templates-strongly-recommended + app.component('fa-icon', FontAwesomeIcon); + app.component('vv-form', Form); + app.component('vv-field', Field); } -renderAsyncComponent('Hello', () => import('./components/Hello.vue')); +// use this file to render top-level components asynchronously. + +// for example: allows calling in HTML! +renderAsyncComponent('Hello', () => import('./components/Hello.vue'), configure); diff --git a/templates/vue3/client/js/vue-renderer.ts b/templates/vue3/client/js/vue-renderer.ts new file mode 100644 index 00000000..844a3374 --- /dev/null +++ b/templates/vue3/client/js/vue-renderer.ts @@ -0,0 +1,54 @@ +import Vue from 'vue'; + +type VueAsync = () => Promise; + +type ConfigureVueApp = (app: Vue.App) => void; + +/** + * Convert a hyphenated string to camelCase. + */ +function hyphenToCamelCase(name: string) { + return name.replace(/-(.)/g, (_match: string, char: string) => { + return char.toUpperCase(); + }); +} + +/** + * Attempts to extract element attributes as string map. + * @param el + * @returns + */ +function convertElementAttributesToPropsMap(el: Element): Record { + if (el.hasAttributes() === false) { + return {}; + } + const result: Record = {}; + + for (const attribute of el.attributes) { + const name = hyphenToCamelCase(attribute.name); + result[name] = attribute.value; + } + + return result; +} + +/** + * For each matching HTML Elements, render and mount a Vue Component asynchronously. + * Passes Element attributes as string to props. Kebab-case attributes will be converted to camel-case. + * @param tag + * @param factory + * @param configure + */ +export function renderAsyncComponent(tag: string, factory: VueAsync, configure?: ConfigureVueApp) { + const ac = Vue.defineAsyncComponent(factory); + const elements = document.getElementsByTagName(tag); + + for (const el of elements) { + const props = convertElementAttributesToPropsMap(el) + const app = Vue.createApp(ac, props); + if (configure) { + configure(app); + } + app.mount(el); + } +} diff --git a/templates/vue3/package.json b/templates/vue3/package.json index ce82cf50..43726590 100644 --- a/templates/vue3/package.json +++ b/templates/vue3/package.json @@ -6,28 +6,32 @@ "output": "wwwroot" }, "dependencies": { - "@types/luxon": "^1.25.0", - "@types/requirejs": "^2.1.32", - "axios": "^0.21.0", - "bootstrap": "^4.5.3", - "jquery": "^3.5.1", + "@fortawesome/fontawesome-svg-core": "^1.2.35", + "@fortawesome/free-regular-svg-icons": "^5.15.3", + "@fortawesome/free-solid-svg-icons": "^5.15.3", + "@fortawesome/vue-fontawesome": "3.0.0-3", + "@popperjs/core": "^2.9.2", + "@types/luxon": "^1.26.3", + "@vee-validate/rules": "^4.1.20", + "axios": "^0.21.1", + "bootstrap": "5.0.0-beta3", "linq": "^3.2.3", - "luxon": "^1.25.0", - "popper.js": "^1.16.1", - "sweetalert2": "^10.10.1", + "luxon": "^1.26.0", + "sweetalert2": "^10.15.7", "ts-polyfill": "^3.8.2", - "tslib": "^2.0.3", + "tslib": "^2.2.0", + "vee-validate": "^4.2.4", "vue": "^3.0.2" }, "devDependencies": { - "@typescript-eslint/eslint-plugin": "4.8.1", - "@typescript-eslint/parser": "4.8.1", - "@vue/compiler-sfc": "3.0.2", - "eslint": "7.13.0", - "eslint-config-prettier": "^6.15.0", - "eslint-plugin-vue": "^7.1.0", - "typescript": "^4.1.2", - "vue-eslint-parser": "^7.1.1", - "vue-loader": "16.0.0-rc.2" + "@typescript-eslint/eslint-plugin": "4.21.0", + "@typescript-eslint/parser": "4.21.0", + "@vue/compiler-sfc": "3.0.11", + "eslint": "7.23.0", + "eslint-config-prettier": "^8.1.0", + "eslint-plugin-vue": "^7.8.0", + "typescript": "^4.2.3", + "vue-eslint-parser": "^7.6.0", + "vue-loader": "16.2.0" } }