Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(esl-utils): extend attr decorator with inherit option #2228

Merged
merged 9 commits into from
Feb 26, 2024
17 changes: 12 additions & 5 deletions src/modules/esl-utils/decorators/attr.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {identity, resolveProperty} from '../misc/functions';
import {parseString, toKebabCase} from '../misc/format';
import {getAttr, setAttr} from '../dom/attr';
import {getAttr, getClosestAttr, setAttr} from '../dom/attr';

import type {PropertyProvider} from '../misc/functions';
import type {ESLAttributeDecorator} from '../dom/attr';
Expand All @@ -15,6 +15,13 @@ type AttrDescriptor<T = string> = {
name?: string;
/** Create getter only */
readonly?: boolean;
/** Find value to inherit across closest elements in DOM tree based on declared attribute name (in case of string format)
* or same attribute name of current element (boolean value).
* Example, attribute 'ignore' with configuration:
* inherit: 'alt-ignore' - searches ignore or data-ignore attr (in case of dataAttr: true) on this element or alt-ignore attribute on closest parent
* inherit: true - searches ignore or data-ignore attr (in case of dataAttr: true) on this element or on closest parent
*/
ala-n marked this conversation as resolved.
Show resolved Hide resolved
inherit?: boolean | string;
/** Use data-* attribute */
dataAttr?: boolean;
/** Default property value. Used if no attribute is present on the element. Empty string by default. Supports provider function. */
Expand All @@ -36,14 +43,14 @@ const buildAttrName =
export const attr = <T = string>(config: AttrDescriptor<T> = {}): ESLAttributeDecorator => {
return (target: ESLDomElementTarget, propName: string): any => {
const attrName = buildAttrName(config.name || propName, !!config.dataAttr);
const inheritAttrName = typeof config.inherit === 'string' ? config.inherit : attrName;

function get(): T | null {
const val = getAttr(this, attrName);
if (val === null && 'defaultValue' in config) {
return resolveProperty(config.defaultValue, this) as T;
}
const val = config.inherit ? getClosestAttr(this, inheritAttrName) || getAttr(this, attrName) : getAttr(this, attrName);
ala-n marked this conversation as resolved.
Show resolved Hide resolved
if (val === null && 'defaultValue' in config) return resolveProperty(config.defaultValue, this) as T;
return (config.parser || parseString as AttrParser<any>)(val);
}

function set(value: T): void {
setAttr(this, attrName, (config.serializer as AttrSerializer<any> || identity)(value));
}
Expand Down
97 changes: 97 additions & 0 deletions src/modules/esl-utils/decorators/test/attr.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,103 @@ describe('Decorator: attr', () => {
expect(el.defProvider).toBe('');
});

describe('inherit parameter', () => {
ala-n marked this conversation as resolved.
Show resolved Hide resolved
class FirstElement extends HTMLElement {}
customElements.define('first-el', FirstElement);
const el1 = new FirstElement();

class SecondElement extends HTMLElement {
@attr({inherit: 'box'})
public container: string;
@attr({inherit: 'parent', dataAttr: true})
public ignore: string;
@attr({inherit: true})
public disallow: string;
@attr({inherit: true, dataAttr: true})
public allow: string;
}

customElements.define('second-el', SecondElement);
const el2 = new SecondElement();

beforeAll(() => {
el1.append(el2);
document.body.append(el1);
});
ala-n marked this conversation as resolved.
Show resolved Hide resolved

describe('value of inherit is presented in string format', () => {
ala-n marked this conversation as resolved.
Show resolved Hide resolved
test('declared inherit is on this element', () => {
el2.setAttribute('box', 'value');
expect(el2.container).toBe('value');
});
test('change of inherit value leads to change of getter value', () => {
el2.setAttribute('box', 'container');
expect(el2.container).toBe('container');
});
test('declared inherit is found on the closest element in DOM', () => {
el2.removeAttribute('box');
el1.setAttribute('box', 'carousel');
expect(el2.container).toBe('carousel');
});
test('elements with declared inherit are absent in DOM and returns empty string', () => {
el1.removeAttribute('box');
expect(el2.container).toBe('');
});
});

describe('value of inherit is presented in string format with data-prefix', () => {
test('declared closest is on this element', () => {
el2.setAttribute('data-ignore', 'swipe');
expect(el2.ignore).toBe('swipe');
});
test('change of inherit value leads to change of getter value', () => {
el2.setAttribute('data-ignore', 'touch');
expect(el2.ignore).toBe('touch');
});
test('declared inherit is found on the closest element in DOM', () => {
el2.removeAttribute('data-ignore');
el1.setAttribute('parent', 'close');
expect(el2.ignore).toBe('close');
});
test('elements with declared inherit are absent in DOM and returns empty string', () => {
el1.removeAttribute('parent');
expect(el2.ignore).toBe('');
});
});

describe('inherit is boolean', () => {
test('declared inherit is on this element', () => {
el2.setAttribute('disallow', 'scroll');
expect(el2.disallow).toBe('scroll');
});
test('declared inherit is found on the closest element in DOM', () => {
el2.removeAttribute('disallow');
el1.setAttribute('disallow', 'activator');
expect(el2.disallow).toBe('activator');
});
test('elements with declared inherit are absent in DOM and returns empty string', () => {
el1.removeAttribute('disallow');
expect(el2.disallow).toBe('');
});
});

describe('inherit is boolean with data prefix', () => {
test('declared inherit is on this element', () => {
el2.setAttribute('data-allow', 'option');
expect(el2.allow).toBe('option');
});
test('declared inherit is found on the closest element in DOM', () => {
el2.removeAttribute('data-allow');
el1.setAttribute('data-allow', 'scroll');
expect(el2.allow).toBe('scroll');
});
test('elements with declared inherit are absent in DOM and returns empty string', () => {
el1.removeAttribute('data-allow');
expect(el2.allow).toBe('');
});
});
});

afterAll(() => {
document.body.removeChild(el);
});
Expand Down
7 changes: 7 additions & 0 deletions src/modules/esl-utils/dom/attr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,10 @@ export function setAttr($el: ESLAttributeTarget, name: string, value: undefined
$el.setAttribute(name, value === true ? '' : value);
}
}

/** Gets attribute value from the closest element with group behavior settings */
export function getClosestAttr($el: ESLAttributeTarget, attrName: string): string | null {
if (!($el = resolveDomTarget($el))) return null;
const $closest = $el.closest(`[${attrName}]`);
return $closest ? $closest.getAttribute(attrName) : null;
}
26 changes: 25 additions & 1 deletion src/modules/esl-utils/dom/test/attr.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {hasAttr, getAttr, setAttr} from '../attr';
import {hasAttr, getAttr, setAttr, getClosestAttr} from '../attr';

describe('Attribute', () => {
const attrName = 'test-attr';
Expand Down Expand Up @@ -118,4 +118,28 @@ describe('Attribute', () => {
expect($el3.getAttribute(attrName)).toBe(attrValue);
});
});


describe('getClosestAttr', () => {
const $el = document.createElement('div');
$el.setAttribute(attrName, attrValue);

const $parent = document.createElement('div');
const $parentName = 'parent-attr';
const $parentValue = 'parent-value';
$parent.setAttribute($parentName, $parentValue);

$parent.append($el);
document.body.append($parent);

test('finds indicated attribute on this element', () => {
expect(getClosestAttr($el, attrName)).toBe(attrValue);
ala-n marked this conversation as resolved.
Show resolved Hide resolved
});
test('finds indicated attribute on closest parent in DOM', () => {
expect(getClosestAttr($el, $parentName)).toBe($parentValue);
ala-n marked this conversation as resolved.
Show resolved Hide resolved
});
test('returns null in case indicated attribute is absent in DOM', () => {
expect(getClosestAttr($el, 'name')).toBe(null);
ala-n marked this conversation as resolved.
Show resolved Hide resolved
});
});
});
Loading