Browse Source

Add ComponentMatcher to find methods

new-plugin-system
Artur Arseniev 4 months ago
parent
commit
e2c21df182
  1. 14
      docs/api/component.md
  2. 53
      packages/core/src/dom_components/model/Component.ts
  3. 5
      packages/core/src/dom_components/model/types.ts
  4. 98
      packages/core/test/specs/dom_components/model/Component.ts

14
docs/api/component.md

@ -177,13 +177,17 @@ also before rendering the component
### Parameters
* `type` **[String][1]** Component type
* `query` **([String][1] | Function)** Component type or matcher function
* `opts` **[Object][2]?** Search options
* `opts.max` **[Number][12]?** Maximum number of matches before exiting
### Examples
```javascript
const allImages = component.findType('image');
console.log(allImages[0]) // prints the first found component
const someComponents = component.findType((cmp) => cmp.getType() === 'something', { max: 2 });
```
Returns **[Array][5]\<Component>**&#x20;
@ -195,7 +199,7 @@ If no component is found, it returns `undefined`.
### Parameters
* `type` **[String][1]** Component type
* `query` **([String][1] | Function)** Component type or matcher function
### Examples
@ -204,6 +208,8 @@ const image = component.findFirstType('image');
if (image) {
console.log(image);
}
const firstImage = component.findFirstType((cmp) => cmp.is('image'));
```
Returns **(Component | [undefined][11])**&#x20;
@ -234,13 +240,15 @@ also before rendering the component
### Parameters
* `type` **[String][1]** Component type
* `query` **([String][1] | Function)** Component type or matcher function
### Examples
```javascript
const Section = component.closestType('section');
console.log(Section);
const namedSection = component.closestType((cmp) => cmp.getName() === 'Section');
```
Returns **Component** Found component, otherwise `undefined`

53
packages/core/src/dom_components/model/Component.ts

@ -62,6 +62,8 @@ import {
ComponentAdd,
ComponentDefinition,
ComponentDefinitionDefined,
ComponentFindOptions,
ComponentMatcher,
ComponentOptions,
ComponentProperties,
DragMode,
@ -79,6 +81,11 @@ export interface CheckIdOptions {
updatedIds?: Record<string, ComponentDefinitionDefined[]>;
}
const getComponentMatcher = (query: ComponentMatcher) => (isString(query) ? (cmp: Component) => cmp.is(query) : query);
const getComponentFindMax = ({ max }: ComponentFindOptions = {}) =>
typeof max === 'number' && isFinite(max) && max > 0 ? Math.max(1, Math.floor(max)) : undefined;
const escapeRegExp = (str: string) => {
return str.replace(/[|\\{}()[\]^$+*?.]/g, '\\$&');
};
@ -620,19 +627,40 @@ export default class Component extends StyleableModel<ComponentProperties> {
* Find all inner components by component type.
* The advantage of this method over `find` is that you can use it
* also before rendering the component
* @param {String} type Component type
* @param {String|Function} query Component type or matcher function
* @param {Object} [opts={}] Search options
* @param {Number} [opts.max] Maximum number of matches before exiting
* @returns {Array<Component>}
* @example
* const allImages = component.findType('image');
* console.log(allImages[0]) // prints the first found component
* const someComponents = component.findType((cmp) => cmp.getType() === 'something', { max: 2 });
*/
findType(type: string) {
findType(query: ComponentMatcher, opts: ComponentFindOptions = {}) {
const result: Component[] = [];
const find = (components: Components) =>
const matcher = getComponentMatcher(query);
const max = getComponentFindMax(opts);
const find = (components: Components) => {
let stop = false;
components.forEach((item) => {
item.is(type) && result.push(item);
find(item.components());
if (stop) return;
if (matcher(item)) {
result.push(item);
if (max && result.length >= max) {
stop = true;
return;
}
}
stop = find(item.components()) || stop;
});
return stop;
};
find(this.components());
return result;
}
@ -640,16 +668,17 @@ export default class Component extends StyleableModel<ComponentProperties> {
/**
* Find the first inner component by component type.
* If no component is found, it returns `undefined`.
* @param {String} type Component type
* @param {String|Function} query Component type or matcher function
* @returns {Component|undefined}
* @example
* const image = component.findFirstType('image');
* if (image) {
* console.log(image);
* }
* const firstImage = component.findFirstType((cmp) => cmp.is('image'));
*/
findFirstType(type: string): Component | undefined {
return this.findType(type).at(0);
findFirstType(query: ComponentMatcher): Component | undefined {
return this.findType(query, { max: 1 }).at(0);
}
/**
@ -670,16 +699,18 @@ export default class Component extends StyleableModel<ComponentProperties> {
* Find the closest parent component by its type.
* The advantage of this method over `closest` is that you can use it
* also before rendering the component
* @param {String} type Component type
* @param {String|Function} query Component type or matcher function
* @returns {Component} Found component, otherwise `undefined`
* @example
* const Section = component.closestType('section');
* console.log(Section);
* const namedSection = component.closestType((cmp) => cmp.getName() === 'Section');
*/
closestType(type: string) {
closestType(query: ComponentMatcher) {
const matcher = getComponentMatcher(query);
let parent = this.parent();
while (parent && !parent.is(type)) {
while (parent && !matcher(parent)) {
parent = parent.parent();
}

5
packages/core/src/dom_components/model/types.ts

@ -17,6 +17,11 @@ import { ParsedNode } from '../../parser/types';
export type DragMode = 'translate' | 'absolute' | '';
export type DraggableDroppableFn = (source: Component, target: Component, index?: number) => boolean | void;
export type ComponentMatcher = string | ((cmp: Component) => boolean);
export interface ComponentFindOptions {
max?: number;
}
export interface AddComponentsOption extends AddOptions, OptionAsDocument {}

98
packages/core/test/specs/dom_components/model/Component.ts

@ -273,6 +273,63 @@ describe('Component', () => {
expect(result.class).toEqual(undefined);
});
test('findType returns all matching components in depth-first order', () => {
const image1 = new ComponentImage({}, compOpts);
const group = new Component({ type: 'group' }, compOpts);
const text = new ComponentText({}, compOpts);
const image2 = new ComponentImage({}, compOpts);
group.append([text, image2]);
obj.append([image1, group]);
expect(obj.findType('image')).toEqual([image1, image2]);
});
test('findType accepts a predicate matcher', () => {
const target1 = new Component({ type: 'something' }, compOpts);
const group = new Component({ type: 'group' }, compOpts);
const target2 = new Component({ type: 'something' }, compOpts);
const text = new ComponentText({}, compOpts);
group.append([text, target2]);
obj.append([target1, group]);
expect(obj.findType((cmp) => cmp.getType() === 'something')).toEqual([target1, target2]);
});
test('findType supports max occurrences', () => {
const target1 = new Component({ type: 'something' }, compOpts);
const group = new Component({ type: 'group' }, compOpts);
const target2 = new Component({ type: 'something' }, compOpts);
const target3 = new Component({ type: 'something' }, compOpts);
group.append(target2);
obj.append([target1, group, target3]);
expect(obj.findType((cmp) => cmp.getType() === 'something', { max: 2 })).toEqual([target1, target2]);
});
test('findType stops traversing once max occurrences are reached', () => {
const target = new Component({ type: 'something' }, compOpts);
const nested = new Component({ type: 'nested' }, compOpts);
const sibling = new Component({ type: 'other' }, compOpts);
let calls = 0;
target.append(nested);
obj.append([target, sibling]);
const result = obj.findType(
(cmp) => {
calls++;
return cmp.getType() === 'something';
},
{ max: 1 },
);
expect(result).toEqual([target]);
expect(calls).toBe(1);
});
test('findFirstType returns first component of specified type', () => {
const image1 = new ComponentImage({}, compOpts);
const text = new ComponentText({}, compOpts);
@ -299,6 +356,47 @@ describe('Component', () => {
expect(result).toBeUndefined();
});
test('findFirstType accepts a predicate matcher', () => {
const text = new ComponentText({}, compOpts);
const image = new ComponentImage({}, compOpts);
obj.append([text, image]);
expect(obj.findFirstType((cmp) => cmp.is('image'))).toBe(image);
});
test('findFirstType returns undefined for a missing predicate match', () => {
const text = new ComponentText({}, compOpts);
obj.append(text);
expect(obj.findFirstType((cmp) => cmp.is('image'))).toBeUndefined();
});
test('closestType accepts a predicate matcher', () => {
const section = new Component({ type: 'section' }, compOpts);
const group = new Component({ type: 'group' }, compOpts);
const image = new ComponentImage({}, compOpts);
group.append(image);
section.append(group);
obj.append(section);
expect(image.closestType((cmp) => cmp.getType() === 'section')).toBe(section);
});
test('closestType still accepts a string matcher', () => {
const section = new Component({ type: 'section' }, compOpts);
const group = new Component({ type: 'group' }, compOpts);
const image = new ComponentImage({}, compOpts);
group.append(image);
section.append(group);
obj.append(section);
expect(image.closestType('section')).toBe(section);
});
test('setAttributes', () => {
obj.setAttributes({
id: 'test',

Loading…
Cancel
Save