├── .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 |
118 |
119 |
120 |
121 |
135 | ```
136 |
137 | Loading Button with ILoading
138 |
139 | ```vue
140 |
141 |
145 |
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 |
--------------------------------------------------------------------------------