Vue typescript class and decorator based component system with support for ES class inheritance and Vue 3 composition API integration.
—
Mixins, custom decorators, TypeScript JSX support, and utility functions for advanced Vue component composition.
Function for creating mixins with multiple Vue component classes, supporting full ES class inheritance.
/**
* Creates a mixin class from multiple Vue component classes
* @param conses - Array of component constructor classes to mix
* @returns Mixed class with combined functionality
*/
function mixins<T extends VueCons[]>(...conses: T): MixedClass<T>;
type MixedClass<Mixins extends VueCons[], Base extends VueCons = VueCons> =
Mixins extends [infer T extends VueCons, ...infer E extends VueCons[]] ?
MixedClass<E, VueCons<InstanceType<Base> & InstanceType<T>, MergeIdentityType<InstanceType<T>[typeof IdentitySymbol], InstanceType<Base>[typeof IdentitySymbol]>>> :
Base;Usage Examples:
import { Component, mixins, Setup, Prop } from "vue-facing-decorator";
import { ref } from "vue";
// Define base mixins
@Component
class LoggingMixin {
log(message: string) {
console.log(`[${this.constructor.name}] ${message}`);
}
created() {
this.log("Component created");
}
}
@Component
class CounterMixin {
@Setup(() => ref(0))
count!: number;
increment() {
this.count++;
this.log?.(`Count incremented to ${this.count}`);
}
decrement() {
this.count--;
this.log?.(`Count decremented to ${this.count}`);
}
}
@Component
class TimestampMixin {
@Setup(() => ref(Date.now()))
createdAt!: number;
getAge() {
return Date.now() - this.createdAt;
}
}
// Combine mixins
@Component
class MixedComponent extends mixins(LoggingMixin, CounterMixin, TimestampMixin) {
@Prop({ type: String, required: true })
title!: string;
mounted() {
this.log(`Mounted with title: ${this.title}`);
this.log(`Component age: ${this.getAge()}ms`);
}
handleClick() {
this.increment();
this.log(`Click handled, age: ${this.getAge()}ms`);
}
}
// Multiple inheritance chains
@Component
class BaseFeature {
baseMethod() {
return "from base";
}
}
@Component
class FeatureA extends BaseFeature {
featureAMethod() {
return "from feature A";
}
}
@Component
class FeatureB extends BaseFeature {
featureBMethod() {
return "from feature B";
}
}
@Component
class CombinedFeatures extends mixins(FeatureA, FeatureB) {
combinedMethod() {
return `${this.baseMethod()}, ${this.featureAMethod()}, ${this.featureBMethod()}`;
}
}Factory function for creating custom decorators with full integration into the Vue component system.
/**
* Factory for creating custom decorators
* @param creator - Function that modifies the Vue component option
* @param options - Optional configuration for the decorator
* @returns Custom decorator function
*/
function createDecorator(
creator: Creator,
options?: { preserve?: boolean }
): PropertyDecorator;
type Creator = (options: any, key: string) => void;
interface CustomDecoratorRecord {
key: string;
creator: Creator;
preserve: boolean;
}Usage Examples:
import { Component, createDecorator, Setup } from "vue-facing-decorator";
import { ref } from "vue";
// Create custom validation decorator
const Validate = createDecorator((option: any, key: string) => {
// Add validation logic to component options
option.methods = option.methods || {};
option.methods[`validate${key.charAt(0).toUpperCase() + key.slice(1)}`] = function() {
// Custom validation logic
return this[key] !== null && this[key] !== undefined;
};
});
// Create custom logging decorator
const AutoLog = createDecorator((option: any, key: string) => {
option.watch = option.watch || {};
option.watch[key] = {
handler(newVal: any, oldVal: any) {
console.log(`${key} changed from`, oldVal, 'to', newVal);
},
immediate: false
};
});
// Create debounced decorator
const Debounced = (delay: number) => createDecorator((option: any, key: string) => {
const originalMethod = option.methods?.[key];
if (originalMethod) {
let timeoutId: any;
option.methods[key] = function(...args: any[]) {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
originalMethod.apply(this, args);
}, delay);
};
}
});
// Usage of custom decorators
@Component
class CustomDecoratorComponent {
@Validate()
@AutoLog()
@Setup(() => ref(""))
username!: string;
@AutoLog()
@Setup(() => ref(0))
score!: number;
@Debounced(300)
search() {
console.log("Searching for:", this.username);
}
mounted() {
// Validation method was auto-generated
console.log("Username valid:", this.validateUsername());
}
}
// Persistent decorator that preserves original behavior
const Trackable = createDecorator((option: any, key: string) => {
// Track property access
option.created = option.created || [];
if (!Array.isArray(option.created)) {
option.created = [option.created];
}
option.created.push(function() {
console.log(`Trackable property "${key}" initialized`);
});
}, { preserve: true });
@Component
class TrackableComponent {
@Trackable()
@Setup(() => ref("tracked value"))
trackedProp!: string;
}Generic function providing TypeScript support for JSX/TSX components with full type inference.
/**
* Generic function for TypeScript JSX/TSX type support
* @returns Type-enhanced component decorator
*/
function TSX<
Properties extends IdentityType['props'] = {},
Events extends IdentityType['events'] = {},
IT extends IdentityType = { props: Properties; events: Events }
>(): <C extends VueCons>(cons: C) => VueCons<InstanceType<C>, MergeIdentityType<IT, InstanceType<C>[typeof IdentitySymbol]>>;
interface IdentityType {
props: Record<string, any>;
events: Record<string, any>;
}Usage Examples:
import { Component, TSX, Prop, Emit } from "vue-facing-decorator";
import { h, VNode } from "vue";
// Define types for props and events
interface ButtonProps {
variant: 'primary' | 'secondary';
disabled?: boolean;
size?: 'small' | 'medium' | 'large';
}
interface ButtonEvents {
click: (event: MouseEvent) => void;
focus: () => void;
blur: () => void;
}
// Apply TSX typing
@TSX<ButtonProps, ButtonEvents>()
@Component
class TypedButton {
@Prop({ type: String, required: true, validator: (v: string) => ['primary', 'secondary'].includes(v) })
variant!: 'primary' | 'secondary';
@Prop({ type: Boolean, default: false })
disabled!: boolean;
@Prop({ type: String, default: 'medium', validator: (v: string) => ['small', 'medium', 'large'].includes(v) })
size!: 'small' | 'medium' | 'large';
@Emit('click')
handleClick(event: MouseEvent) {
if (!this.disabled) {
return event;
}
}
@Emit('focus')
handleFocus() {
console.log('Button focused');
}
@Emit('blur')
handleBlur() {
console.log('Button blurred');
}
render(): VNode {
return h('button', {
class: [
'btn',
`btn--${this.variant}`,
`btn--${this.size}`,
{ 'btn--disabled': this.disabled }
],
disabled: this.disabled,
onClick: this.handleClick,
onFocus: this.handleFocus,
onBlur: this.handleBlur
}, this.$slots.default?.());
}
}
// Complex component with nested types
interface FormData {
username: string;
email: string;
age: number;
}
interface FormProps {
initialData?: Partial<FormData>;
readonly?: boolean;
}
interface FormEvents {
submit: (data: FormData) => void;
change: (field: keyof FormData, value: any) => void;
reset: () => void;
}
@TSX<FormProps, FormEvents>()
@Component
class TypedForm {
@Prop({ type: Object, default: () => ({}) })
initialData!: Partial<FormData>;
@Prop({ type: Boolean, default: false })
readonly!: boolean;
private formData: FormData = {
username: '',
email: '',
age: 0,
...this.initialData
};
@Emit('submit')
handleSubmit() {
return { ...this.formData };
}
@Emit('change')
handleFieldChange(field: keyof FormData, value: any) {
this.formData[field] = value as any;
return { field, value };
}
@Emit('reset')
handleReset() {
this.formData = { username: '', email: '', age: 0, ...this.initialData };
}
render(): VNode {
return h('form', {
onSubmit: (e: Event) => {
e.preventDefault();
this.handleSubmit();
}
}, [
h('input', {
value: this.formData.username,
disabled: this.readonly,
onInput: (e: Event) => this.handleFieldChange('username', (e.target as HTMLInputElement).value)
}),
h('input', {
type: 'email',
value: this.formData.email,
disabled: this.readonly,
onInput: (e: Event) => this.handleFieldChange('email', (e.target as HTMLInputElement).value)
}),
h('input', {
type: 'number',
value: this.formData.age,
disabled: this.readonly,
onInput: (e: Event) => this.handleFieldChange('age', parseInt((e.target as HTMLInputElement).value))
}),
h('button', { type: 'submit' }, 'Submit'),
h('button', { type: 'button', onClick: this.handleReset }, 'Reset')
]);
}
}The advanced features leverage a sophisticated type system for full TypeScript integration:
// Core identity system for type preservation
interface Identity<IT extends IdentityType = { props: {}, events: {} }> {
readonly [IdentitySymbol]: IT;
}
// Type merging utility for mixins
type MergeIdentityType<A extends IdentityType, B extends IdentityType> = {
props: A['props'] & B['props'];
events: A['events'] & B['events'];
};
// Vue constructor with identity preservation
type VueCons<
RawInstance extends Identity = Identity,
IT extends IdentityType = { props: {}, events: {} }
> = {
new(): ComponentPublicInstance<IT['props'] & EventHandlers<IT['events']>> &
Identity<IT> &
Omit<RawInstance, typeof IdentitySymbol>;
};
// Event handler type generation
type EventHandlers<Events extends Record<string, any>> = {
[K in keyof Events as `on${Capitalize<K & string>}`]?:
Events[K] extends Function ? Events[K] : (param: Events[K]) => any;
};Complete Advanced Example:
import { Component, mixins, createDecorator, TSX, Setup, Prop, Emit } from "vue-facing-decorator";
import { ref, computed } from "vue";
// Custom decorator for automatic validation
const AutoValidate = createDecorator((option: any, key: string) => {
option.computed = option.computed || {};
option.computed[`${key}Valid`] = function() {
const value = this[key];
return value !== null && value !== undefined && value !== '';
};
});
// Logging mixin
@Component
class LoggingMixin {
log(level: 'info' | 'warn' | 'error', message: string) {
console[level](`[${this.constructor.name}] ${message}`);
}
}
// Validation mixin
@Component
class ValidationMixin {
@Setup(() => ref<string[]>([]))
errors!: string[];
addError(field: string, message: string) {
this.errors.push(`${field}: ${message}`);
}
clearErrors() {
this.errors.splice(0);
}
get isValid() {
return this.errors.length === 0;
}
}
// Complex component with all advanced features
interface ContactFormProps {
mode: 'create' | 'edit';
initialData?: { name: string; email: string; phone: string };
}
interface ContactFormEvents {
save: (data: { name: string; email: string; phone: string }) => void;
cancel: () => void;
validate: (isValid: boolean) => void;
}
@TSX<ContactFormProps, ContactFormEvents>()
@Component
class AdvancedContactForm extends mixins(LoggingMixin, ValidationMixin) {
@Prop({ type: String, required: true })
mode!: 'create' | 'edit';
@Prop({ type: Object, default: () => ({ name: '', email: '', phone: '' }) })
initialData!: { name: string; email: string; phone: string };
@AutoValidate()
@Setup(() => ref(''))
name!: string;
@AutoValidate()
@Setup(() => ref(''))
email!: string;
@AutoValidate()
@Setup(() => ref(''))
phone!: string;
@Setup(() => computed(() => this.nameValid && this.emailValid && this.phoneValid && this.isValid))
formValid!: boolean;
created() {
this.log('info', `Form created in ${this.mode} mode`);
Object.assign(this, this.initialData);
}
@Emit('save')
handleSave() {
this.clearErrors();
this.validateForm();
if (this.formValid) {
this.log('info', 'Form saved successfully');
return { name: this.name, email: this.email, phone: this.phone };
} else {
this.log('warn', 'Form validation failed');
return null;
}
}
@Emit('cancel')
handleCancel() {
this.log('info', 'Form cancelled');
}
@Emit('validate')
validateForm() {
this.clearErrors();
if (!this.name) this.addError('name', 'Name is required');
if (!this.email) this.addError('email', 'Email is required');
if (!this.phone) this.addError('phone', 'Phone is required');
return this.formValid;
}
render() {
return h('form', { class: 'contact-form' }, [
h('h2', this.mode === 'create' ? 'Create Contact' : 'Edit Contact'),
h('input', {
placeholder: 'Name',
value: this.name,
onInput: (e: Event) => this.name = (e.target as HTMLInputElement).value
}),
h('input', {
type: 'email',
placeholder: 'Email',
value: this.email,
onInput: (e: Event) => this.email = (e.target as HTMLInputElement).value
}),
h('input', {
type: 'tel',
placeholder: 'Phone',
value: this.phone,
onInput: (e: Event) => this.phone = (e.target as HTMLInputElement).value
}),
this.errors.length > 0 && h('ul', { class: 'errors' },
this.errors.map(error => h('li', error))
),
h('div', { class: 'buttons' }, [
h('button', { type: 'button', onClick: this.handleSave }, 'Save'),
h('button', { type: 'button', onClick: this.handleCancel }, 'Cancel')
])
]);
}
}Install with Tessl CLI
npx tessl i tessl/npm-vue-facing-decorator