├── .editorconfig ├── .eslintrc ├── .gitignore ├── .travis.yml ├── README.md ├── __tests__ ├── setup.ts └── vuequery.spec.ts ├── dist ├── .gitkeep ├── vuequery.js └── vuequery.min.js ├── jest.config.js ├── package.json ├── rollup.config.js ├── src ├── helpers.ts ├── index.ts └── vuequery.ts ├── tsconfig.json ├── types.d.ts └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | [*] 2 | charset = utf-8 3 | indent_size = 2 4 | indent_style = space 5 | insert_final_newline = true 6 | trim_trailing_whitespace = true 7 | 8 | [*.md] 9 | trim_trailing_whitespace = false 10 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@typescript-eslint/parser", 3 | "extends": [ 4 | "plugin:@typescript-eslint/recommended" 5 | ], 6 | "parserOptions": { 7 | "ecmaVersion": 2020, 8 | "sourceType": "module" 9 | }, 10 | "rules": { 11 | "@typescript-eslint/member-delimiter-style": 0, 12 | "@typescript-eslint/ban-ts-ignore": 0, 13 | "@typescript-eslint/no-explicit-any": 0 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: [10, 12, 13, 14] 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # VueQuery [![Build Status](https://travis-ci.org/phanan/vuequery.svg?branch=master)](https://travis-ci.org/phanan/vuequery) 2 | 3 | > Traverse [Vue](https://vuejs.org)'s component tree with ease. 4 | 5 | > IMPORTANT: Vue's reactivity/event system is extremely powerful and flexible, and should have 99.99% of your use cases covered. In fact, having to traverse the component tree _almost always_ means you're doing Vue wrong. There are certain edge cases, however, when such is required, and this library aims to aid you there. 6 | 7 | ## Installation & Usage 8 | 9 | > This plugin is only tested for Vue 2. 10 | 11 | ### Installation 12 | 13 | #### Browser 14 | Include `vuequery.min.js` as a script: 15 | 16 | ```html 17 | 18 | ``` 19 | 20 | Now `VueQuery` should be avaiable as a global function. 21 | 22 | #### Node.js 23 | 24 | First, require VueQuery as a dependency with `npm` or `yarn`: 25 | 26 | ``` bash 27 | npm install vuequery 28 | yarn add vuequery 29 | ``` 30 | 31 | Then, import it: 32 | 33 | ``` js 34 | import VueQuery from 'vuequery' 35 | // You can also alias the import as $ for a jQuery-like experience 36 | import $ from 'vuequery' 37 | ``` 38 | 39 | ### Usage 40 | 41 | Similar to jQuery, calling `VueQuery()` on a Vue component returns a VueQuery instance which exposes several API's to let you traverse through Vue's component tree. 42 | 43 | ``` js 44 | // assuming we're currently in a Vue component context 45 | // init a VueQuery instance on the current component 46 | const $vm = VueQuery(this) 47 | 48 | // get the original Vue component 49 | $vm.vm 50 | 51 | // get the immediate next sibling of the component 52 | $vm.next() 53 | 54 | // chaining is supported 55 | $vm.prev('foo').children()[0].find('bar') 56 | ``` 57 | 58 | ## API 59 | 60 | > As VueQuery is heavily inspired by jQuery, its API signatures are very similar (albeit much less sophisticated) to that of jQuery's traversing module. In fact, the documentation here is more or less copied from [jQuery's](http://api.jquery.com/). Differences, if any, will be explicitly specified. 61 | 62 | ### `children([selector])` 63 | 64 | **Description**: Get the children of the current component, optionally filtered by a `selector`, which can be either 65 | 66 | * a string, in which case it will match the components by name. Obviously, for this to work, your components should have the `name` option. This is a good Vue practice anyway. 67 | * a Vue instance 68 | * a VueQuery instance, in which case it will match the encapsulated Vue instance 69 | 70 | **Return Values**: `Array.|[]` 71 | 72 | ### `closest(selector)` 73 | 74 | **Description**: Get the first component that matches the selector by testing the current component itself and traversing up through its ancestors in the component tree. 75 | 76 | **Return Values**: `VueQuery|null` 77 | 78 | ### `find(selector)` 79 | 80 | **Description**: Get the descendants of the current component, filtered by a selector. 81 | 82 | **Return Values**: `Array.|[]` 83 | 84 | ### `has(selector)` 85 | 86 | > Note: This API's behavior is different from its jQuery counterpart. 87 | 88 | **Description**: Check if the current component has any component that matches the selector in its descendant tree. 89 | 90 | **Return Values**: `Boolean` 91 | 92 | ### `is(selector)` 93 | 94 | **Description**: Check if the current component matches the selector. 95 | 96 | **Return Values**: `Boolean` 97 | 98 | ### `next([selector])` 99 | 100 | **Description**: Get the immediately following sibling of the current component. If a selector is provided, it retrieves the next sibling only if it matches that selector. 101 | 102 | **Return Values**: `VueQuery|null` 103 | 104 | ### `nextAll([selector])` 105 | 106 | **Description**: Get all following siblings of the current component, optionally filtered by a selector. 107 | 108 | **Return Values**: `Array.|[]` 109 | 110 | ### `nextUntil([selector][, filter])` 111 | 112 | **Description**: Get all following siblings of the current component, up to but not including the component matched by the selector passed. 113 | If the selector is not matched or is not supplied, all following siblings will be selected; in these cases it selects the same components as the `.nextAll()` method does when no selector is provided. 114 | 115 | The method optionally accepts a `filter` expression for its second argument. If this argument is supplied, the components will be filtered by testing whether they match it. 116 | 117 | **Return Values**: `Array.|[]` 118 | 119 | ### `parent([selector])` 120 | 121 | **Description**: Get the parent of the current component. If the selector is supplied, the parent component will only be returned if it matches it. 122 | 123 | **Return Values**: `VueQuery|null` 124 | 125 | ### `parents([selector])` 126 | 127 | **Description**: Get the ancestors of the current component, optionally filtered by a selector. 128 | 129 | **Return Values**: `Array.|[]` 130 | 131 | ### `parentsUntil([selector][, filter])` 132 | 133 | **Description**: Get the ancestors of the current component, up to but not including the component matched by the selector passed. 134 | If the selector is not matched or is not supplied, all ancestors will be selected; in these cases it selects the same components as the `.parents()` method does when no selector is provided. 135 | 136 | The method optionally accepts a `filter` expression for its second argument. If this argument is supplied, the components will be filtered by testing whether they match it. 137 | 138 | **Return Values**: `Array.|[]` 139 | 140 | ### `prev([selector])` 141 | 142 | **Description**: Get the immediately preceding sibling of the current component. If a selector is provided, it retrieves the previous sibling only if it matches that selector. 143 | 144 | **Return Values**: `VueQuery|null` 145 | 146 | ### `prevAll([selector])` 147 | 148 | **Description**: Get all preceding siblings of the current component, optionally filtered by a selector. 149 | 150 | **Return Values**: `Array.|[]` 151 | 152 | ### `prevUntil([selector][, filter])` 153 | 154 | **Description**: Get all preceding siblings of the current component, up to but not including the component matched by the selector passed. 155 | If the selector is not matched or is not supplied, all preceding siblings will be selected; in these cases it selects the same components as the `.nextAll()` method does when no selector is provided. 156 | 157 | The method optionally accepts a `filter` expression for its second argument. If this argument is supplied, the components will be filtered by testing whether they match it. 158 | 159 | ### `siblings([selector])` 160 | 161 | **Description**: Get the siblings of the current component, optionally filtered by a selector. The original component is not included among the siblings. 162 | 163 | **Return Values**: `Array.|[]` 164 | 165 | ## License 166 | 167 | MIT © [Phan An](http://phanan.net) 168 | -------------------------------------------------------------------------------- /__tests__/setup.ts: -------------------------------------------------------------------------------- 1 | const globalVue = require('vue/dist/vue.js') 2 | 3 | globalVue.component('noop', { 4 | template: '', 5 | }) 6 | 7 | globalVue.component('foo', { 8 | name: 'foo', 9 | template: '

This is foo

', 10 | }) 11 | 12 | globalVue.component('bar', { 13 | name: 'bar', 14 | template: '
', 15 | }) 16 | 17 | globalVue.component('baz', { 18 | name: 'baz', 19 | template: '
', 20 | }) 21 | 22 | globalVue.component('qux', { 23 | name: 'qux', 24 | template: '
', 25 | }) 26 | -------------------------------------------------------------------------------- /__tests__/vuequery.spec.ts: -------------------------------------------------------------------------------- 1 | const globalVue = require('vue/dist/vue.js') 2 | import { VueQuery } from '../types' 3 | import $ from '../src/vuequery' 4 | 5 | describe('VueQuery', () => { 6 | let $vm: VueQuery 7 | 8 | beforeEach(() => { 9 | $vm = $( 10 | new globalVue({ 11 | el: document.createElement('div'), 12 | template: '
', 13 | }).$mount() 14 | ) as VueQuery 15 | }) 16 | 17 | describe('common', () => { 18 | it('doesnt init on a VueQuery instance', () => { 19 | expect($($vm)).toBe($vm) 20 | }) 21 | }) 22 | 23 | describe('children()', () => { 24 | it('matches existing children', () => { 25 | expect($vm.children()).toHaveLength(1) // 26 | expect($vm.children()[0].children()).toHaveLength(4) // 27 | }) 28 | 29 | it('does not match non-existing children', () => { 30 | expect($vm.children('cuckoo')).toHaveLength(0) 31 | }) 32 | }) 33 | 34 | describe('find()', () => { 35 | it('finds direct descendants', () => { 36 | expect($vm.find('qux')).toHaveLength(1) 37 | }) 38 | 39 | it('finds distant descendants', () => { 40 | expect($vm.find('bar')).toHaveLength(3) 41 | }) 42 | 43 | it('doesnt find non-existing name', () => { 44 | expect($vm.find('cuckoo')).toHaveLength(0) 45 | }) 46 | }) 47 | 48 | describe('closest()', () => { 49 | it('finds closest to be self', () => { 50 | expect($vm.find('qux')[0].closest('qux')!.is('qux')).toBe(true) 51 | }) 52 | 53 | it('finds closest to be parent', () => { 54 | expect($vm.find('foo')[0].closest('bar')!.is('bar')).toBe(true) 55 | }) 56 | 57 | it('finds closest to be ancestor', () => { 58 | expect($vm.find('foo')[0].closest('qux')!.is('qux')).toBe(true) 59 | }) 60 | 61 | it('doesnt find invalid closest', () => { 62 | expect($vm.find('foo')[0].closest('cuckoo')).toBeNull() 63 | }) 64 | }) 65 | 66 | describe('has()', () => { 67 | it('has direct descendant', () => { 68 | expect($vm.has('qux')).toBe(true) 69 | }) 70 | 71 | it('has distant descendant', () => { 72 | expect($vm.has('bar')).toBe(true) 73 | }) 74 | 75 | it('doesnt have invalid descendant', () => { 76 | expect($vm.has('cuckoo')).toBe(false) 77 | }) 78 | }) 79 | 80 | describe('is()', () => { 81 | it('is itself', () => { 82 | expect($vm.is($vm)).toBe(true) 83 | }) 84 | 85 | it('is of a name', () => { 86 | expect($vm.find('qux')[0].is('qux')).toBe(true) 87 | }) 88 | }) 89 | 90 | describe('next()', () => { 91 | it('finds next without name', () => { 92 | expect($vm.find('baz')[0].next()!.is('noop')).toBe(true) 93 | }) 94 | 95 | it('finds next with name', () => { 96 | expect($vm.find('baz')[0].next('noop')!.is('noop')).toBe(true) 97 | }) 98 | 99 | it('doesnt find next with invalid name', () => { 100 | expect($vm.find('baz')[0].next('baz')).toBeNull() 101 | }) 102 | 103 | it('doesnt find next at end', () => { 104 | expect($vm.find('baz')[2].next()).toBeNull() 105 | }) 106 | 107 | it('doesnt find next with invalid name', () => { 108 | expect($vm.find('baz')[0].next('cuckoo')).toBeNull() 109 | }) 110 | }) 111 | 112 | describe('nextAll()', () => { 113 | it('finds all next without name', () => { 114 | const collected = $vm.find('baz')[0].nextAll() 115 | expect(collected).toHaveLength(3) 116 | expect(collected[0].is('noop')).toBe(true) 117 | expect(collected[1].is('baz')).toBe(true) 118 | expect(collected[2].is('baz')).toBe(true) 119 | }) 120 | 121 | it('finds all next with name', () => { 122 | const collected = $vm.find('baz')[0].nextAll('baz') 123 | expect(collected).toHaveLength(2) 124 | expect(collected[0].is('baz')).toBe(true) 125 | expect(collected[1].is('baz')).toBe(true) 126 | }) 127 | 128 | it('doesnt find any next with invalid name', () => { 129 | const collected = $vm.find('baz')[0].nextAll('cuckoo') 130 | expect(collected).toHaveLength(0) 131 | }) 132 | }) 133 | 134 | describe('nextUntil()', () => { 135 | it('finds all next until', () => { 136 | const collected = $vm.find('baz')[0].nextUntil('baz') 137 | expect(collected).toHaveLength(1) 138 | expect(collected[0].is('noop')).toBe(true) 139 | }) 140 | 141 | it('finds no next until', () => { 142 | expect($vm.find('baz')[0].nextUntil('baz', 'baz')).toHaveLength(0) 143 | }) 144 | }) 145 | 146 | describe('parent()', () => { 147 | it('finds parent', () => { 148 | expect($vm.find('baz')[0].parent()!.is('qux')) 149 | }) 150 | 151 | it('finds parent with name specified', () => { 152 | expect($vm.find('baz')[0].parent('qux')!.is('qux')).toBe(true) 153 | }) 154 | 155 | it('doesnt find parent with invalid name specified', () => { 156 | expect($vm.find('baz')[0].parent('cuckoo')).toBeNull() 157 | }) 158 | 159 | it('doesnt find parent of root', () => { 160 | expect($vm.parent()).toBeNull() 161 | }) 162 | }) 163 | 164 | describe('parents()', () => { 165 | it('finds direct parents', () => { 166 | const parents = $vm.find('foo')[0].parents('bar') 167 | expect(parents).toHaveLength(1) 168 | expect(parents[0].is('bar')).toBe(true) 169 | }) 170 | 171 | it('finds distant parents', () => { 172 | const parents = $vm.find('foo')[0].parents('baz') 173 | expect(parents).toHaveLength(1) 174 | expect(parents[0].is('baz')).toBe(true) 175 | }) 176 | 177 | it('find parents without name specified', () => { 178 | const parents = $vm.find('foo')[0].parents() 179 | expect(parents).toHaveLength(4) 180 | expect(parents[0].is('bar')).toBe(true) 181 | expect(parents[1].is('baz')).toBe(true) 182 | expect(parents[2].is('qux')).toBe(true) 183 | expect(parents[3].vm === parents[3].vm.$root).toBe(true) 184 | }) 185 | 186 | it('doesnt find parents with invalid name specified', () => { 187 | expect($vm.find('baz')[0].parents('cuckoo')).toHaveLength(0) 188 | }) 189 | 190 | it('doesnt find parents of root', () => { 191 | expect($vm.parents()).toHaveLength(0) 192 | }) 193 | }) 194 | 195 | describe('parentsUntil()', () => { 196 | it('finds parents until', () => { 197 | const parents = $vm.find('foo')[0].parentsUntil('qux') 198 | expect(parents).toHaveLength(2) 199 | expect(parents[0].is('bar')).toBe(true) 200 | expect(parents[1].is('baz')).toBe(true) 201 | }) 202 | 203 | it('doesnt finds parents of root', () => { 204 | expect($vm.parentsUntil()).toHaveLength(0) 205 | }) 206 | }) 207 | 208 | describe('prev()', () => { 209 | it('finds prev without name', () => { 210 | expect($vm.find('baz')[1].prev()!.is('noop')).toBe(true) 211 | }) 212 | 213 | it('finds prev with name', () => { 214 | expect($vm.find('baz')[1].prev('noop')!.is('noop')).toBe(true) 215 | }) 216 | 217 | it('doesnt find prev with invalid name', () => { 218 | expect($vm.find('baz')[1].prev('baz')).toBeNull() 219 | }) 220 | 221 | it('doesnt find prev at beginning', () => { 222 | expect($vm.find('baz')[0].prev()).toBeNull() 223 | }) 224 | 225 | it('doesnt find prev with invalid name', () => { 226 | expect($vm.find('baz')[1].prev('cuckoo')).toBeNull() 227 | }) 228 | }) 229 | 230 | describe('prevAll()', () => { 231 | it('finds all prev without name', () => { 232 | const collected = $vm.find('baz')[2].prevAll() 233 | expect(collected).toHaveLength(3) 234 | expect(collected[0].is('baz')).toBe(true) 235 | expect(collected[1].is('noop')).toBe(true) 236 | expect(collected[2].is('baz')).toBe(true) 237 | }) 238 | 239 | it('finds all prev with name', () => { 240 | const collected = $vm.find('baz')[2].prevAll('baz') 241 | expect(collected).toHaveLength(2) 242 | expect(collected[0].is('baz')).toBe(true) 243 | expect(collected[1].is('baz')).toBe(true) 244 | }) 245 | 246 | it('doesnt find any prev with invalid name', () => { 247 | expect($vm.find('baz')[2].prevAll('cuckoo')).toHaveLength(0) 248 | }) 249 | }) 250 | 251 | describe('prevUntil()', () => { 252 | it('finds all prev until', () => { 253 | const collected = $vm.find('baz')[1].prevUntil('baz') 254 | expect(collected).toHaveLength(1) 255 | expect(collected[0].is('noop')).toBe(true) 256 | }) 257 | 258 | it('finds no prev until', () => { 259 | expect($vm.find('baz')[1].nextUntil('baz', 'baz')).toHaveLength(0) 260 | }) 261 | }) 262 | 263 | describe('siblings()', () => { 264 | it('finds all siblings', () => { 265 | const me = $vm.find('baz')[1] 266 | const collected = me.siblings() 267 | expect(collected).toHaveLength(3) 268 | expect(collected).not.toContain(me) 269 | }) 270 | }) 271 | }) 272 | -------------------------------------------------------------------------------- /dist/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phanan/vuequery/1696b6d4b94c2e4b0cc1cf508be7b95ba05461f7/dist/.gitkeep -------------------------------------------------------------------------------- /dist/vuequery.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | function isVueQuery(selector) { 4 | return selector._isVueQuery === true; 5 | } 6 | /** 7 | * Checks if a Vue component matches against a selector. 8 | * The selector can be either: 9 | * - a string (name of the component) 10 | * - a VueComponent object 11 | * - a VueQuery object 12 | */ 13 | var matches = function (vm, selector) { 14 | if (!selector) { 15 | return true; 16 | } 17 | if (selector instanceof String || typeof selector === 'string') { 18 | return vm.$options.name === selector; 19 | } 20 | if (isVueQuery(selector)) { 21 | return selector.vm === vm; 22 | } 23 | return selector === vm; 24 | }; 25 | /** 26 | * Get all children of a Vue component, optionally filtered by a selector. 27 | */ 28 | var rawChildren = function (vm, selector) { 29 | var collected = []; 30 | vm.$children.forEach(function (child) { 31 | if (matches(child, selector)) { 32 | collected.push(child); 33 | } 34 | }); 35 | return collected; 36 | }; 37 | /** 38 | * Get all siblings of a Vue component, optionally filtered by a selector. 39 | * @param {Boolean} withSelf Whether to include the current vm 40 | */ 41 | var rawSiblings = function (vm, selector, withSelf) { 42 | if (withSelf === void 0) { withSelf = true; } 43 | if (isRoot(vm)) { 44 | return []; 45 | } 46 | var collected = rawChildren(vm.$parent, selector); 47 | if (!withSelf) { 48 | var index = collected.indexOf(vm); 49 | if (index > -1) { 50 | collected.splice(index, 1); 51 | } 52 | } 53 | return collected; 54 | }; 55 | /** 56 | * Check if a Vue component is the root instance. 57 | */ 58 | var isRoot = function (vm) { 59 | return vm.$root === vm; 60 | }; 61 | /** 62 | * Get the first sibling of a Vue component filtered by a selector. 63 | */ 64 | var siblingsWithSort = function (vm, selector, direction) { 65 | var siblings = rawSiblings(vm); 66 | if (!siblings.length) { 67 | return null; 68 | } 69 | var i = siblings.indexOf(vm); 70 | if (direction === "Ascending") { 71 | if (i === siblings.length - 1) { 72 | return null; 73 | } 74 | return matches(siblings[i + 1], selector) ? siblings[i + 1] : null; 75 | } 76 | if (i === 0) { 77 | return null; 78 | } 79 | return matches(siblings[i - 1], selector) ? siblings[i - 1] : null; 80 | }; 81 | /** 82 | * Get all siblings of a Vue component filtered by a selector. 83 | */ 84 | var siblingsAllWithSort = function (vm, selector, direction) { 85 | var siblings = rawSiblings(vm, selector); 86 | if (!siblings.length) { 87 | return []; 88 | } 89 | var i = siblings.indexOf(vm); 90 | return direction === "Ascending" 91 | ? siblings.slice(i + 1) 92 | : siblings.slice(0, i).reverse(); 93 | }; 94 | /** 95 | * Get all siblings of a Vue component filtered by a "filter" selector, and up to but 96 | * not including the "until" selector passed. 97 | */ 98 | var siblingsUntilWithSort = function (vm, until, filter, direction) { 99 | var siblings = rawSiblings(vm); 100 | if (!siblings.length) { 101 | return []; 102 | } 103 | if (direction === "Descending") { 104 | siblings = siblings.reverse(); 105 | } 106 | siblings = siblings.slice(siblings.indexOf(vm) + 1); 107 | if (!until) { 108 | return siblings; 109 | } 110 | var collected = []; 111 | for (var i = 0, j = siblings.length; i < j; ++i) { 112 | if (matches(siblings[i], until)) { 113 | break; 114 | } 115 | if (matches(siblings[i], filter)) { 116 | collected.push(siblings[i]); 117 | } 118 | } 119 | return collected; 120 | }; 121 | 122 | var $ = function (vm) { 123 | if (Array.isArray(vm)) { 124 | // If the argument is an array, we VueQuery'fy the elements. 125 | return vm.map(function (c) { 126 | return $(c); 127 | }); 128 | } 129 | // Avoid double encapsulation 130 | if (isVueQuery(vm)) { 131 | return vm; 132 | } 133 | // @ts-ignore 134 | if (!vm._isVue) { 135 | throw new Error('[VueQuery] Cannot instantiate on a non-Vue element'); 136 | } 137 | if (!isRoot(vm) && !vm.$options.name) { 138 | throw new Error('[VueQuery] Non-root component must have a `name` option'); 139 | } 140 | return { 141 | vm: vm, 142 | _isVueQuery: true, 143 | children: function (selector) { 144 | return $(rawChildren(vm, selector)); 145 | }, 146 | closest: function (selector) { 147 | if (matches(vm, selector)) { 148 | return $(vm); 149 | } 150 | if (!vm.$parent) { 151 | return null; 152 | } 153 | var $parent = $(vm.$parent); 154 | return $parent ? $parent.closest(selector) : null; 155 | }, 156 | find: function (selector) { 157 | var _find = function (component) { 158 | var collected = []; 159 | if (!component.$children || !component.$children.length) { 160 | return []; 161 | } 162 | component.$children.forEach(function (c) { 163 | if (matches(c, selector)) { 164 | collected.push(c); 165 | } 166 | collected = collected.concat(_find(c)); 167 | }); 168 | return collected; 169 | }; 170 | return $(_find(vm)); 171 | }, 172 | has: function (selector) { 173 | if (!vm.$children || !vm.$children.length) { 174 | return false; 175 | } 176 | for (var i = 0, j = vm.$children.length; i < j; ++i) { 177 | if (matches(vm.$children[i], selector)) { 178 | return true; 179 | } 180 | var $c = $(vm.$children[i]); 181 | if (!$c) { 182 | break; 183 | } 184 | return $c.has(selector); 185 | } 186 | return false; 187 | }, 188 | is: function (selector) { 189 | if (Array.isArray(selector)) { 190 | return selector.some(function (item) { return matches(vm, item); }); 191 | } 192 | return matches(vm, selector); 193 | }, 194 | next: function (selector) { 195 | var _next = siblingsWithSort(vm, selector ? selector : null, 'Ascending'); 196 | return _next ? $(_next) : null; 197 | }, 198 | nextAll: function (selector) { 199 | return $(siblingsAllWithSort(vm, selector ? selector : null, 'Ascending')); 200 | }, 201 | nextUntil: function (until, filter) { 202 | return $(siblingsUntilWithSort(vm, until, filter, 'Ascending')); 203 | }, 204 | parent: function (selector) { 205 | if (isRoot(vm)) { 206 | return null; 207 | } 208 | return matches(vm.$parent, selector) ? $(vm.$parent) : null; 209 | }, 210 | parents: function (selector) { 211 | var _parents = function (component) { 212 | if (isRoot(component)) { 213 | return []; 214 | } 215 | var collected = []; 216 | if (matches(component.$parent, selector)) { 217 | collected.push(component.$parent); 218 | } 219 | collected = collected.concat(_parents(component.$parent)); 220 | return collected; 221 | }; 222 | return $(_parents(vm)); 223 | }, 224 | parentsUntil: function (until, filter) { 225 | var _parentsUntil = function (component) { 226 | if (isRoot(component)) { 227 | return []; 228 | } 229 | var collected = []; 230 | if (until && matches(component.$parent, until)) { 231 | return collected; 232 | } 233 | if (matches(component.$parent, filter)) { 234 | collected.push(component.$parent); 235 | } 236 | return collected.concat(_parentsUntil(component.$parent)); 237 | }; 238 | return $(_parentsUntil(vm)); 239 | }, 240 | prev: function (selector) { 241 | var _prev = siblingsWithSort(vm, selector ? selector : null, 'Descending'); 242 | return _prev ? $(_prev) : null; 243 | }, 244 | prevAll: function (selector) { 245 | return $(siblingsAllWithSort(vm, selector ? selector : null, 'Descending')); 246 | }, 247 | prevUntil: function (until, filter) { 248 | return $(siblingsUntilWithSort(vm, until, filter, 'Descending')); 249 | }, 250 | siblings: function (selector) { 251 | var children = rawSiblings(vm, selector); 252 | var index = children.indexOf(vm); 253 | if (index > -1) { 254 | children.splice(index, 1); 255 | } 256 | return $(children); 257 | } 258 | }; 259 | }; 260 | 261 | module.exports = $; 262 | -------------------------------------------------------------------------------- /dist/vuequery.min.js: -------------------------------------------------------------------------------- 1 | var VueQuery=function(){"use strict";function n(n){return!0===n._isVueQuery}var r=function(r,e){return!e||(e instanceof String||"string"==typeof e?r.$options.name===e:n(e)?e.vm===r:e===r)},e=function(n,e){var t=[];return n.$children.forEach((function(n){r(n,e)&&t.push(n)})),t},t=function(n,r,t){if(void 0===t&&(t=!0),u(n))return[];var i=e(n.$parent,r);if(!t){var c=i.indexOf(n);c>-1&&i.splice(c,1)}return i},u=function(n){return n.$root===n},i=function(n,e,u){var i=t(n);if(!i.length)return null;var c=i.indexOf(n);return"Ascending"===u?c===i.length-1?null:r(i[c+1],e)?i[c+1]:null:0===c?null:r(i[c-1],e)?i[c-1]:null},c=function(n,r,e){var u=t(n,r);if(!u.length)return[];var i=u.indexOf(n);return"Ascending"===e?u.slice(i+1):u.slice(0,i).reverse()},o=function(n,e,u,i){var c=t(n);if(!c.length)return[];if("Descending"===i&&(c=c.reverse()),c=c.slice(c.indexOf(n)+1),!e)return c;for(var o=[],l=0,a=c.length;l-1&&r.splice(e,1),l(r)}}};return l}(); 2 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest', 3 | setupFilesAfterEnv: ['/__tests__/setup.ts'], 4 | testMatch: ['**/__tests__/**/*.spec.ts'] 5 | } 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vuequery", 3 | "version": "2.1.1", 4 | "description": "Traverse Vue's component tree with ease", 5 | "main": "dist/vuequery.js", 6 | "scripts": { 7 | "lint": "eslint", 8 | "test": "eslint && jest", 9 | "build": "rollup -c" 10 | }, 11 | "keywords": [ 12 | "vue", 13 | "vue.js", 14 | "vuejs", 15 | "component", 16 | "components", 17 | "traverse", 18 | "query" 19 | ], 20 | "author": "Phan An (https://phanan.net)", 21 | "license": "MIT", 22 | "types": "./types.d.ts", 23 | "peerDependencies": { 24 | "vue": "~2.5.0" 25 | }, 26 | "devDependencies": { 27 | "@rollup/plugin-typescript": "^4.1.1", 28 | "@types/jest": "^25.2.2", 29 | "@typescript-eslint/eslint-plugin": "^2.33.0", 30 | "@typescript-eslint/parser": "^2.33.0", 31 | "eslint": "^7.0.0", 32 | "jest": "^26.0.1", 33 | "rollup": "^2.10.2", 34 | "rollup-plugin-terser": "^5.3.0", 35 | "ts-jest": "^26.0.0", 36 | "typescript": "^3.9.2", 37 | "vue": "~2.5.0" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import typescript from '@rollup/plugin-typescript' 2 | import { terser } from 'rollup-plugin-terser' 3 | 4 | export default { 5 | input: 'src/index.ts', 6 | output: [ 7 | { 8 | file: 'dist/vuequery.js', 9 | format: 'cjs' 10 | }, 11 | { 12 | file: 'dist/vuequery.min.js', 13 | format: 'iife', 14 | plugins: [terser()], 15 | name: 'VueQuery' 16 | } 17 | ], 18 | external: ['vue'], 19 | plugins: [typescript()] 20 | } 21 | -------------------------------------------------------------------------------- /src/helpers.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import { Selector, VueQuery, SortDirection } from '../types' 3 | 4 | export function isVueQuery(selector: Selector): selector is VueQuery { 5 | return (selector as VueQuery)._isVueQuery === true 6 | } 7 | 8 | /** 9 | * Checks if a Vue component matches against a selector. 10 | * The selector can be either: 11 | * - a string (name of the component) 12 | * - a VueComponent object 13 | * - a VueQuery object 14 | */ 15 | export const matches = (vm: Vue, selector?: Selector | null): boolean => { 16 | if (!selector) { 17 | return true 18 | } 19 | 20 | if (selector instanceof String || typeof selector === 'string') { 21 | return vm.$options.name === selector 22 | } 23 | 24 | if (isVueQuery(selector)) { 25 | return selector.vm === vm 26 | } 27 | 28 | return selector === vm 29 | } 30 | 31 | /** 32 | * Get all children of a Vue component, optionally filtered by a selector. 33 | */ 34 | export const rawChildren = (vm: Vue, selector?: Selector | null): Vue[] => { 35 | const collected: Vue[] = [] 36 | 37 | vm.$children.forEach((child) => { 38 | if (matches(child, selector)) { 39 | collected.push(child) 40 | } 41 | }) 42 | 43 | return collected 44 | } 45 | 46 | /** 47 | * Get all siblings of a Vue component, optionally filtered by a selector. 48 | * @param {Boolean} withSelf Whether to include the current vm 49 | */ 50 | export const rawSiblings = ( 51 | vm: Vue, 52 | selector?: Selector | null, 53 | withSelf = true 54 | ): Vue[] => { 55 | if (isRoot(vm)) { 56 | return [] 57 | } 58 | 59 | const collected = rawChildren(vm.$parent, selector) 60 | 61 | if (!withSelf) { 62 | const index = collected.indexOf(vm) 63 | 64 | if (index > -1) { 65 | collected.splice(index, 1) 66 | } 67 | } 68 | 69 | return collected 70 | } 71 | 72 | /** 73 | * Check if a Vue component is the root instance. 74 | */ 75 | export const isRoot = (vm: Vue): boolean => { 76 | return vm.$root === vm 77 | } 78 | 79 | /** 80 | * Get the first sibling of a Vue component filtered by a selector. 81 | */ 82 | export const siblingsWithSort = ( 83 | vm: Vue, 84 | selector: Selector | null, 85 | direction: SortDirection 86 | ): Vue | null => { 87 | const siblings = rawSiblings(vm) 88 | 89 | if (!siblings.length) { 90 | return null 91 | } 92 | 93 | const i = siblings.indexOf(vm) 94 | 95 | if (direction === "Ascending") { 96 | if (i === siblings.length - 1) { 97 | return null 98 | } 99 | 100 | return matches(siblings[i + 1], selector) ? siblings[i + 1] : null 101 | } 102 | 103 | if (i === 0) { 104 | return null 105 | } 106 | 107 | return matches(siblings[i - 1], selector) ? siblings[i - 1] : null 108 | } 109 | 110 | /** 111 | * Get all siblings of a Vue component filtered by a selector. 112 | */ 113 | export const siblingsAllWithSort = ( 114 | vm: Vue, 115 | selector: Selector | null, 116 | direction: SortDirection 117 | ): Vue[] => { 118 | const siblings = rawSiblings(vm, selector) 119 | 120 | if (!siblings.length) { 121 | return [] 122 | } 123 | 124 | const i = siblings.indexOf(vm) 125 | 126 | return direction === "Ascending" 127 | ? siblings.slice(i + 1) 128 | : siblings.slice(0, i).reverse() 129 | } 130 | 131 | /** 132 | * Get all siblings of a Vue component filtered by a "filter" selector, and up to but 133 | * not including the "until" selector passed. 134 | */ 135 | export const siblingsUntilWithSort = ( 136 | vm: Vue, 137 | until: Selector | undefined, 138 | filter: Selector | undefined, 139 | direction: SortDirection 140 | ): Vue[] => { 141 | let siblings = rawSiblings(vm) 142 | 143 | if (!siblings.length) { 144 | return [] 145 | } 146 | 147 | if (direction === "Descending") { 148 | siblings = siblings.reverse() 149 | } 150 | 151 | siblings = siblings.slice(siblings.indexOf(vm) + 1) 152 | 153 | if (!until) { 154 | return siblings 155 | } 156 | 157 | const collected = [] 158 | 159 | for (let i = 0, j = siblings.length; i < j; ++i) { 160 | if (matches(siblings[i], until)) { 161 | break 162 | } 163 | 164 | if (matches(siblings[i], filter)) { 165 | collected.push(siblings[i]) 166 | } 167 | } 168 | 169 | return collected 170 | } 171 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import $ from './vuequery' 2 | 3 | export default $ 4 | -------------------------------------------------------------------------------- /src/vuequery.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import { VueQuery, Selector } from '../types' 3 | 4 | import { 5 | isRoot, 6 | matches, 7 | rawSiblings, 8 | rawChildren, 9 | siblingsWithSort, 10 | siblingsUntilWithSort, 11 | siblingsAllWithSort, 12 | isVueQuery 13 | } from './helpers' 14 | 15 | const $ = (vm: Vue | VueQuery | (Vue | VueQuery)[] | any): VueQuery | VueQuery[] => { 16 | if (Array.isArray(vm)) { 17 | // If the argument is an array, we VueQuery'fy the elements. 18 | return vm.map((c: Vue | VueQuery): VueQuery => { 19 | return $(c) as VueQuery 20 | }) 21 | } 22 | 23 | // Avoid double encapsulation 24 | if (isVueQuery(vm)) { 25 | return vm 26 | } 27 | 28 | // @ts-ignore 29 | if (!vm._isVue) { 30 | throw new Error('[VueQuery] Cannot instantiate on a non-Vue element') 31 | } 32 | 33 | if (!isRoot(vm) && !vm.$options.name) { 34 | throw new Error('[VueQuery] Non-root component must have a `name` option') 35 | } 36 | 37 | return { 38 | vm, 39 | _isVueQuery: true, 40 | 41 | children (selector?: Selector): VueQuery[] { 42 | return $(rawChildren(vm, selector)) as VueQuery[] 43 | }, 44 | 45 | closest (selector: Selector): VueQuery | null { 46 | if (matches(vm, selector)) { 47 | return $(vm) as VueQuery 48 | } 49 | 50 | if (!vm.$parent) { 51 | return null 52 | } 53 | 54 | const $parent = $(vm.$parent) as VueQuery 55 | 56 | return $parent ? $parent.closest(selector) : null 57 | }, 58 | 59 | find (selector: Selector): VueQuery[] { 60 | const _find = (component: Vue): Vue[] => { 61 | let collected: Vue[] = [] 62 | 63 | if (!component.$children || !component.$children.length) { 64 | return [] 65 | } 66 | 67 | component.$children.forEach(c => { 68 | if (matches(c, selector)) { 69 | collected.push(c) 70 | } 71 | 72 | collected = collected.concat(_find(c)) 73 | }) 74 | 75 | return collected 76 | } 77 | 78 | return $(_find(vm)) as VueQuery[] 79 | }, 80 | 81 | has (selector: Selector): boolean { 82 | if (!vm.$children || !vm.$children.length) { 83 | return false 84 | } 85 | 86 | for (let i = 0, j = vm.$children.length; i < j; ++i) { 87 | if (matches(vm.$children[i], selector)) { 88 | return true 89 | } 90 | 91 | const $c = $(vm.$children[i]) as VueQuery 92 | 93 | if (!$c) { 94 | break 95 | } 96 | 97 | return $c.has(selector) 98 | } 99 | 100 | return false 101 | }, 102 | 103 | is (selector: Selector | (Extract)[]): boolean { 104 | if (Array.isArray(selector)) { 105 | return selector.some(item => matches(vm, item)) 106 | } 107 | 108 | return matches(vm, selector) 109 | }, 110 | 111 | next (selector?: Selector): VueQuery | null { 112 | const _next = siblingsWithSort(vm, selector ? selector : null, 'Ascending') 113 | 114 | return _next ? $(_next) as VueQuery : null 115 | }, 116 | 117 | nextAll (selector?: Selector): VueQuery[] { 118 | return $(siblingsAllWithSort(vm, selector ? selector : null, 'Ascending')) as VueQuery[] 119 | }, 120 | 121 | nextUntil (until?: Selector, filter?: Selector): VueQuery[] { 122 | return $(siblingsUntilWithSort(vm, until, filter, 'Ascending')) as VueQuery[] 123 | }, 124 | 125 | parent (selector?: Selector): VueQuery | null { 126 | if (isRoot(vm)) { 127 | return null 128 | } 129 | 130 | return matches(vm.$parent, selector) ? $(vm.$parent) as VueQuery : null 131 | }, 132 | 133 | parents (selector?: Selector): VueQuery[] { 134 | const _parents = (component: Vue): Vue[] => { 135 | if (isRoot(component)) { 136 | return [] 137 | } 138 | 139 | let collected: Vue[] = [] 140 | 141 | if (matches(component.$parent, selector)) { 142 | collected.push(component.$parent) 143 | } 144 | 145 | collected = collected.concat(_parents(component.$parent)) 146 | 147 | return collected 148 | } 149 | 150 | return $(_parents(vm)) as VueQuery[] 151 | }, 152 | 153 | parentsUntil (until?: Selector, filter?: Selector): VueQuery[] { 154 | const _parentsUntil = (component: Vue): Vue[] => { 155 | if (isRoot(component)) { 156 | return [] 157 | } 158 | 159 | const collected: Vue[] = [] 160 | 161 | if (until && matches(component.$parent, until)) { 162 | return collected 163 | } 164 | 165 | if (matches(component.$parent, filter)) { 166 | collected.push(component.$parent) 167 | } 168 | 169 | return collected.concat(_parentsUntil(component.$parent)) 170 | } 171 | 172 | return $(_parentsUntil(vm)) as VueQuery[] 173 | }, 174 | 175 | prev (selector?: Selector): VueQuery | null { 176 | const _prev = siblingsWithSort(vm, selector ? selector : null, 'Descending') 177 | 178 | return _prev ? $(_prev) as VueQuery : null 179 | }, 180 | 181 | prevAll (selector?: Selector): VueQuery[] { 182 | return $(siblingsAllWithSort(vm, selector ? selector : null, 'Descending')) as VueQuery[] 183 | }, 184 | 185 | prevUntil (until?: Selector, filter?: Selector): VueQuery[] { 186 | return $(siblingsUntilWithSort(vm, until, filter, 'Descending')) as VueQuery[] 187 | }, 188 | 189 | siblings (selector?: Selector): VueQuery[] { 190 | const children = rawSiblings(vm, selector) 191 | const index = children.indexOf(vm) 192 | 193 | if (index > -1) { 194 | children.splice(index, 1) 195 | } 196 | 197 | return $(children) as VueQuery[] 198 | } 199 | } 200 | } 201 | 202 | export default $ 203 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "strict": true, 5 | "module": "es2015", 6 | "moduleResolution": "node", 7 | "allowSyntheticDefaultImports": true, 8 | "noImplicitThis": false, 9 | "downlevelIteration": true, 10 | "esModuleInterop": true 11 | }, 12 | "include": [ 13 | "./**/*.ts" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /types.d.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | 3 | export type Selector = string | Vue | VueQuery 4 | 5 | export type SortDirection = 'Ascending' | 'Descending' 6 | 7 | export interface VueQuery { 8 | vm: Vue 9 | _isVueQuery: true 10 | 11 | /** 12 | * Get the children of the current component, optionally filtered by a selector. 13 | */ 14 | children(selector?: Selector): VueQuery[] 15 | 16 | /** 17 | * Get the first component that matches the selector by testing the current component itself 18 | * and traversing up through its ancestors in the component tree. 19 | */ 20 | closest(selector: Selector): VueQuery | null 21 | 22 | /** 23 | * Get the descendants of the current component, filtered by a selector. 24 | */ 25 | find(selector: Selector): VueQuery[] 26 | 27 | /** 28 | * Check if the current component has the selector in its descendant tree. 29 | */ 30 | has(selector: Selector): boolean 31 | 32 | /** 33 | * Check if the current component matches the selector (which can be a string, a 34 | * Vue/VueQuery instance, or an array of those, in which case a single 35 | * match will return true.) 36 | */ 37 | is(selector: Selector): boolean 38 | 39 | /** 40 | * Get the immediately following sibling of the current component. 41 | * If a selector is provided, it retrieves the next sibling only if it matches that selector. 42 | */ 43 | next(selector?: Selector): VueQuery | null 44 | 45 | /** 46 | * Get all following siblings of the current component, optionally filtered by a selector. 47 | */ 48 | nextAll(selector?: Selector): VueQuery[] 49 | 50 | /** 51 | * Get all following siblings of the current component, up to but not including the component 52 | * matched by the selector passed. 53 | */ 54 | nextUntil(until?: Selector, filter?: Selector): VueQuery[] 55 | 56 | /** 57 | * Get the parent of the current component, optionally filtered by a selector. 58 | */ 59 | parent(selector?: Selector): VueQuery | null 60 | 61 | /** 62 | * Get the ancestors of the current component, optionally filtered by a selector. 63 | */ 64 | parents(selector?: Selector): VueQuery[] 65 | 66 | /** 67 | * Get the ancestors of the current component, up to but not including the component matched 68 | * by the selector. 69 | */ 70 | parentsUntil(until?: Selector, filter?: Selector): VueQuery[] 71 | 72 | /** 73 | * Get the immediately preceding sibling of the current component. 74 | * If a selector is provided, it retrieves the previous sibling only if it matches that selector. 75 | */ 76 | prev(selector?: Selector): VueQuery | null 77 | 78 | /** 79 | * Get all preceding siblings of the current component, optionally filtered by a selector. 80 | */ 81 | prevAll(selector?: Selector): VueQuery[] 82 | 83 | /** 84 | * Get all preceding siblings of the current component, up to but not including the component 85 | * matched by the selector. 86 | */ 87 | prevUntil(until?: Selector, filter?: Selector): VueQuery[] 88 | 89 | /** 90 | * Get the siblings of the current component, optionally filtered by a selector. 91 | */ 92 | siblings(selector?: Selector): VueQuery[] 93 | } 94 | 95 | export default function $(vm: Vue | VueQuery | (Vue | VueQuery)[] | any): VueQuery | VueQuery[] 96 | --------------------------------------------------------------------------------