├── .eslintignore ├── .eslintrc.cjs ├── .gitignore ├── LICENSE ├── README.md ├── babel.config.js ├── jest.config.json ├── package.json ├── src ├── index.d.ts └── index.js ├── tests ├── controlGroup │ ├── IBorder.js │ ├── IButton.js │ └── IVisibility.js ├── experimentalGroup │ ├── IBorder.js │ ├── IButton.js │ └── IVisibility.js └── index.spec.js └── yarn.lock /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | es2021: true, 5 | node: true 6 | }, 7 | extends: [ 8 | 'standard' // javascript standard 語法規範。 9 | ], 10 | ignorePatterns: ['node_modules/**', 'dist/**'], // 排除檢查 11 | plugins: ['jest'], 12 | overrides: [ 13 | { 14 | env: { 15 | node: true, 16 | jest: true 17 | }, 18 | files: ['.eslintrc.{js,cjs}', '**/__tests__/*.{j,t}s?(x)', '**/*.spec.{j,t}s?(x)', 'tests/**/*.js'], 19 | plugins: ['jest'], 20 | parserOptions: { 21 | sourceType: 'module' 22 | } 23 | } 24 | ], 25 | parserOptions: { 26 | ecmaVersion: 'latest', 27 | sourceType: 'module' 28 | }, 29 | rules: { 30 | indent: ['error', 2], 31 | quotes: ['error', 'single'], 32 | semi: ['error', 'never'], 33 | 'linebreak-style': ['error', 'unix'], 34 | 'comma-dangle': ['error', 'never'] 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | vscode/extensions.json 18 | vscode/settings.json 19 | .idea 20 | .DS_Store 21 | *.suo 22 | *.ntvs* 23 | *.njsproj 24 | *.sln 25 | *.sw? 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Pedro Yang 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Vue Inheritance 2 | 3 | ### Introduction 4 | 5 | vue-inheritance is an npm package designed for Vue.js projects. It provides a convenient way to manage and reuse component properties and methods. Leveraging Vue's extension and mixin capabilities, this package simplifies the definition and application of component attributes, making it more modular. 6 | 7 | **Installation** 8 | 9 | install vue-inheritance using the following command: 10 | 11 | ```bash 12 | npm install vue-inheritance 13 | ``` 14 | 15 | **Usage** 16 | 17 | In your Vue project, import VueInheritance: 18 | 19 | ``` 20 | import { VueInheritance } from 'vue-inheritance' 21 | ``` 22 | 23 | **Define Interface Modules** 24 | 25 | Define one or more props, methods, computed modules. 26 | 27 | ```javascript 28 | // IControl 29 | export const IControl = { 30 | props: { 31 | disabled: { 32 | type: Boolean, 33 | default: false 34 | } 35 | } 36 | } 37 | 38 | // ITheme 39 | export const ITheme = { 40 | props: { 41 | theme: { 42 | type: String, 43 | default: 'Standard' 44 | } 45 | } 46 | } 47 | 48 | // ILoading 49 | export const ILoading = { 50 | props: { 51 | isLoading: { 52 | type: Boolean, 53 | default: false 54 | } 55 | } 56 | } 57 | 58 | 59 | // IButton 60 | export const IButton = { 61 | extends: VueInheritance.implement(IControl).implement(ITheme) 62 | props: { 63 | size: { 64 | type: String, 65 | default: 'lg' 66 | } 67 | }, 68 | 69 | methods: { 70 | } 71 | } 72 | 73 | 74 | 75 | 76 | ``` 77 | 78 | **Implement** 79 | 80 | In your specific component, use the VueInheritance implement method to apply Interface modules. 81 | 82 | ```javascript 83 | // Button.vue 84 | export default { 85 | extends: VueInheritance.implement(IControl).implement(ITheme), 86 | methods: { 87 | onClick(e) { 88 | this.$emit('click', e) 89 | } 90 | } 91 | } 92 | 93 | // or 94 | 95 | export default { 96 | extends: IButton 97 | } 98 | ``` 99 | 100 | **Extend** 101 | 102 | In another component, use the extend method to inherit an existing component and the implement method to apply additional attribute modules. 103 | 104 | ```javascript 105 | // LoadingButton.vue 106 | import Button from './Button.vue' 107 | 108 | export default { 109 | extends: VueInheritance.extend(Button).implement(ILoading) 110 | } 111 | ``` 112 | 113 | **Examples** 114 | Button with IControl and ITheme 115 | 116 | ```vue 117 | 120 | 121 | 135 | ``` 136 | 137 | Loading Button with ILoading 138 | 139 | ```vue 140 | 146 | 147 | 156 | ``` 157 | 158 | This way, you can define interface modules based on project requirements and flexibly apply and reuse them in your components. 159 | 160 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [['@babel/preset-env', {targets: {node: 'current'}}]], 3 | }; -------------------------------------------------------------------------------- /jest.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "verbose": true, 3 | "testEnvironment": "node", 4 | "transform": { 5 | "^.+\\.js$": "babel-jest" 6 | } 7 | } 8 | 9 | 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-inheritance", 3 | "version": "1.0.1-beta.7", 4 | "description": "vue inheritance", 5 | "main": "src/index.js", 6 | "types": "src/index.d.ts", 7 | "scripts": { 8 | "test": "jest" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/en96321/vue-inheritance.git" 13 | }, 14 | "keywords": [ 15 | "vue", 16 | "extend", 17 | "inherit", 18 | "inhertiance" 19 | ], 20 | "author": "pedro.yang", 21 | "contributors": [ 22 | "ocean.tsai" 23 | ], 24 | "license": "MIT", 25 | "bugs": { 26 | "url": "https://github.com/en96321/vue-Inheritance/issues" 27 | }, 28 | "homepage": "https://github.com/en96321/vue-Inheritance#readme", 29 | "dependencies": { 30 | "ramda": "0.29.1" 31 | }, 32 | "devDependencies": { 33 | "@babel/core": "^7.23.7", 34 | "@babel/preset-env": "^7.23.8", 35 | "babel-jest": "^29.7.0", 36 | "eslint": "^8.56.0", 37 | "eslint-config-standard": "^17.1.0", 38 | "eslint-plugin-import": "^2.25.2", 39 | "eslint-plugin-jest": "^27.6.3", 40 | "eslint-plugin-n": "^16.0.0", 41 | "eslint-plugin-promise": "^6.0.0", 42 | "jest": "^29.7.0", 43 | "vue": "^3.4.15" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/index.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * IVueComponent 3 | * @description Interface for Vue component 4 | * @interface 5 | * @property {Object} emits - Vue emits 6 | * @property {Object} props - Vue props 7 | * @property {Object} methods - Vue methods 8 | * @property {Object} computed - Vue computed 9 | */ 10 | export interface IVueComponent { 11 | emits?: { [key: string]: any }; 12 | props?: { [key: string]: any }; 13 | methods?: { [key: string]: any }; 14 | computed?: { [key: string]: any }; 15 | } 16 | 17 | /** 18 | * VueInheritanceComponent 19 | * @description VueInheritanceComponent class 20 | * @class 21 | */ 22 | export class VueInheritanceComponent { 23 | /** 24 | * implement 25 | * @param {IVueComponent} interfaceDefine - Vue component 26 | * @returns {VueInheritanceComponent} VueInheritanceComponent 27 | */ 28 | implement(interfaceDefine: IVueComponent): VueInheritanceComponent; 29 | } 30 | 31 | /** 32 | * VueInheritance 33 | * @description VueInheritance 34 | * @example 35 | * export default { 36 | * name: 'MyComponent', 37 | * extends: VueInheritance.extend(UIComponent).implement(IScrollable), 38 | * ... 39 | * } 40 | */ 41 | declare const VueInheritance: { 42 | 43 | /** 44 | * extend 45 | * @description Extend, Should only be used once 46 | * @param {IVueComponent} componentDefine - Vue component 47 | * @returns {VueInheritanceComponent} VueInheritanceComponent 48 | */ 49 | extend(componentDefine: IVueComponent): VueInheritanceComponent; 50 | /** 51 | * implement 52 | * @param {IVueComponent} interfaceDefine - Vue component 53 | * @returns {VueInheritanceComponent} VueInheritanceComponent 54 | */ 55 | implement(interfaceDefine: IVueComponent): VueInheritanceComponent; 56 | } 57 | 58 | export default VueInheritance; -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import { clone, isNil, isNotNil, has, isEmpty } from 'ramda' 2 | 3 | /** 4 | * VueInheritanceComponent 5 | * @class 6 | * @private 7 | * @author pedro.yang、 ocean.tsai 8 | * @description 9 | */ 10 | class VueInheritanceComponent { 11 | /** 12 | * InvalidInterfaceKeys 13 | * @static 14 | * @constant 15 | * @private 16 | * @type 17 | * @description interfaceDefine will be checked, if it conforms to the interface, it return true; otherwise, it returns false. 18 | */ 19 | static InvalidInterfaceKeys = Object.freeze([ 20 | 'watch', 21 | 'beforeCreate', 'created', 22 | 'beforeMount', 'mounted', 'beforeUpdate', 'updated', 'beforeUnmount', 'unmounted', 23 | 'errorCaptured', 'renderTracked', 'activated', 'deactivated', 'serverPrefetch' 24 | ]) 25 | 26 | /** 27 | * validateInterface 28 | * @static 29 | * @method 30 | * @protected 31 | * @param {VueInterface} interfaceDefine the param will be checked. 32 | * @description interfaceDefine will be checked, if it conforms to the interface, it return true; otherwise, it returns false. 33 | */ 34 | static validateInterface (interfaceDefine) { 35 | // hasIn 是包含 prototype has 不會含 prototype 36 | return VueInheritanceComponent.InvalidInterfaceKeys.every((key) => !has(key, interfaceDefine)) 37 | } 38 | 39 | /** 40 | * typeCheck 41 | * @static 42 | * @method 43 | * @protected 44 | * @param {VueInterface} interfaceDefine the param will be checked. 45 | * @description interfaceDefine will be checked, if it conforms to the interface, it return true; otherwise, it returns false. 46 | */ 47 | static typeCheck (interfaceDefine) { 48 | if (isNil(interfaceDefine)) { 49 | throw new Error('Interface cannot be null or undefined.') 50 | } else if (isEmpty(interfaceDefine)) { 51 | throw new Error('Interface cannot be empty object.') 52 | } else if (!VueInheritanceComponent.validateInterface(interfaceDefine)) { 53 | throw new Error('The incoming parameter must be an interface.') 54 | } 55 | } 56 | 57 | /** 58 | * extend 59 | * @static 60 | * @method 61 | * @param {VueComponent} componentDefine componentDefine to be inherited. 62 | * @param {VueComponent} override override to be override. 63 | * @description 64 | */ 65 | static extend (componentDefine, override) { 66 | const vueInheritanceComponent = isNotNil(componentDefine) 67 | ? Object.assign(new VueInheritanceComponent(), clone(componentDefine)) 68 | : new VueInheritanceComponent() 69 | 70 | return isNotNil(override) 71 | ? Object.assign(vueInheritanceComponent, clone(override)) 72 | : vueInheritanceComponent 73 | } 74 | 75 | /** 76 | * implement 77 | * @static 78 | * @method 79 | * @public 80 | * @param {VueComponent} interfaceDefine interfaceDefine to be inherited. 81 | * @description Vue's interface can only define props、methods. 82 | */ 83 | static implement (interfaceDefine) { 84 | VueInheritanceComponent.typeCheck(interfaceDefine) 85 | return VueInheritanceComponent.extend(interfaceDefine) 86 | } 87 | 88 | // eslint-disable-next-line no-useless-constructor 89 | constructor (componentOptions) { 90 | } 91 | 92 | /** 93 | * extends 94 | * @type {VueComponent} extends 95 | * @description property of Vue component option api. 96 | */ 97 | extends = null 98 | 99 | /** 100 | * implement 101 | * @static 102 | * @method 103 | * @public 104 | * @param {VueInterface} interface interface to be inherited. 105 | * @description Vue's interface can only define props、methods. 106 | */ 107 | implement (interfaceDefine) { 108 | VueInheritanceComponent.typeCheck(interfaceDefine) 109 | 110 | if (isNil(this.extends) || isEmpty(this.extends)) { 111 | this.extends = clone(interfaceDefine) 112 | } else { 113 | let deepestNode = this.extends 114 | // find deepes node. 115 | while (isNotNil(deepestNode.extends)) { 116 | deepestNode = deepestNode.extends 117 | } 118 | deepestNode.extends = clone(interfaceDefine) 119 | } 120 | return this 121 | } 122 | } 123 | 124 | /** 125 | * VueInheritance 126 | * @static 127 | * @public 128 | * @description 129 | */ 130 | const VueInheritance = Object.freeze({ 131 | extend: VueInheritanceComponent.extend, 132 | implement: VueInheritanceComponent.implement 133 | }) 134 | 135 | export default VueInheritance 136 | -------------------------------------------------------------------------------- /tests/controlGroup/IBorder.js: -------------------------------------------------------------------------------- 1 | import { IVisibility } from './IVisibility.js' 2 | 3 | /** 4 | * IBorder 5 | * @author ocean.tsai 6 | * @public 7 | * @interface 8 | * @description 9 | */ 10 | export const IBorder = { 11 | extends: IVisibility, 12 | props: { 13 | borderSize: { 14 | type: Number, 15 | default: '1px' 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /tests/controlGroup/IButton.js: -------------------------------------------------------------------------------- 1 | import { IBorder } from './IBorder.js' 2 | 3 | /** 4 | * IButton 5 | * @author ocean.tsai 6 | * @public 7 | * @interface 8 | * @description 9 | */ 10 | export const IButton = { 11 | extends: IBorder, 12 | 13 | props: { 14 | buttonSize: { 15 | type: String, 16 | default: 'sm' 17 | } 18 | }, 19 | 20 | methods: { 21 | onClick () { 22 | this.$emit('click', this.value) 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /tests/controlGroup/IVisibility.js: -------------------------------------------------------------------------------- 1 | /** 2 | * IVisibility 3 | * @author ocean.tsai 4 | * @public 5 | * @interface 6 | * @description 7 | */ 8 | export const IVisibility = { 9 | props: { 10 | visible: { 11 | type: Boolean, 12 | default: true 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /tests/experimentalGroup/IBorder.js: -------------------------------------------------------------------------------- 1 | /** 2 | * IBorder 3 | * @author ocean.tsai 4 | * @public 5 | * @interface 6 | * @description 7 | */ 8 | export const IBorder = { 9 | props: { 10 | borderSize: { 11 | type: Number, 12 | default: '1px' 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /tests/experimentalGroup/IButton.js: -------------------------------------------------------------------------------- 1 | import VueInheritance from '../../src/index.js' 2 | import { IBorder } from './IBorder.js' 3 | import { IVisibility } from './IVisibility.js' 4 | /** 5 | * IButton 6 | * @author ocean.tsai 7 | * @public 8 | * @interface 9 | * @description 10 | */ 11 | export const IButton = { 12 | extends: VueInheritance.implement(IBorder).implement(IVisibility), 13 | props: { 14 | buttonSize: { 15 | type: String, 16 | default: 'sm' 17 | } 18 | }, 19 | 20 | methods: { 21 | onClick () { 22 | this.$emit('click', this.value) 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /tests/experimentalGroup/IVisibility.js: -------------------------------------------------------------------------------- 1 | /** 2 | * IVisibility 3 | * @author ocean.tsai 4 | * @public 5 | * @interface 6 | * @description 7 | */ 8 | export const IVisibility = { 9 | props: { 10 | visible: { 11 | type: Boolean, 12 | default: true 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /tests/index.spec.js: -------------------------------------------------------------------------------- 1 | import VueInheritance from '../src/index.js' // 請確保路徑正確 2 | import { IButton as ControlGroupIButton } from './controlGroup/IButton.js' 3 | import { IButton as ExperimentalGroupIButton } from './experimentalGroup/IButton.js' 4 | 5 | describe('VueInheritance', () => { 6 | test('The implementation method should return an object with isShow', () => { 7 | const IVisibility = VueInheritance.implement({ 8 | props: { 9 | isShow: { 10 | type: Boolean, 11 | default: true 12 | } 13 | } 14 | }) 15 | 16 | expect(IVisibility).toHaveProperty('props.isShow', { 17 | type: Boolean, 18 | default: true 19 | }) 20 | 21 | expect(IVisibility).toEqual({ 22 | extends: null, 23 | props: { 24 | isShow: { 25 | type: Boolean, 26 | default: true 27 | } 28 | } 29 | }) 30 | }) 31 | 32 | test('The experimentalGroupIButton test', () => { 33 | expect(ExperimentalGroupIButton).toHaveProperty('extends', { 34 | extends: { 35 | props: { 36 | visible: { 37 | type: Boolean, 38 | default: true 39 | } 40 | } 41 | }, 42 | props: { 43 | borderSize: { 44 | type: Number, 45 | default: '1px' 46 | } 47 | } 48 | }) 49 | expect(ExperimentalGroupIButton).toHaveProperty('props', { 50 | buttonSize: { 51 | type: String, 52 | default: 'sm' 53 | } 54 | }) 55 | expect(ExperimentalGroupIButton).toHaveProperty('methods.onClick') 56 | }) 57 | }) 58 | 59 | test('The ControlGroupIButton test', () => { 60 | expect(ControlGroupIButton).toHaveProperty('extends', { 61 | extends: { 62 | props: { 63 | visible: { 64 | type: Boolean, 65 | default: true 66 | } 67 | } 68 | }, 69 | props: { 70 | borderSize: { 71 | type: Number, 72 | default: '1px' 73 | } 74 | } 75 | }) 76 | expect(ControlGroupIButton).toHaveProperty('props', { 77 | buttonSize: { 78 | type: String, 79 | default: 'sm' 80 | } 81 | }) 82 | expect(ControlGroupIButton).toHaveProperty('methods.onClick') 83 | }) 84 | --------------------------------------------------------------------------------