Skip to content

Commit

Permalink
Merge pull request #39 from swup/feat/attributes
Browse files Browse the repository at this point in the history
Update additional body attributes
  • Loading branch information
daun authored Oct 19, 2024
2 parents f3cde8f + c476cb5 commit ce914bc
Show file tree
Hide file tree
Showing 5 changed files with 136 additions and 7 deletions.
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,15 @@ e.g. `page-`. It will then only update those classes and leave all other classes
prefix: 'page-'
}
```

### attributes

Update additional attributes of the body element. Useful if you have defined the `lang` or `dir`
attributes or work with `data-*` attributes on the body element. Accepts an array of strings or
regular expression instances.

```javascript
{
attributes: ['lang', 'dir', /^data-/]
}
```
29 changes: 29 additions & 0 deletions src/attributes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
export function updateAttributes(
target: Element,
source: Element,
filters: (string | RegExp)[] = []
): void {
const keep = new Set<string>();

for (const { name, value } of getAttributes(source, filters)) {
target.setAttribute(name, value);
keep.add(name);
}

for (const { name } of getAttributes(target, filters)) {
if (!keep.has(name)) {
target.removeAttribute(name);
}
}
}

function getAttributes(el: Element, filters: (string | RegExp)[] = []): Attr[] {
const all = Array.from(el.attributes);
if (!filters.length) return all;

return all.filter(({ name }) =>
filters.some((pattern) =>
pattern instanceof RegExp ? pattern.test(name) : name === pattern
)
);
}
15 changes: 11 additions & 4 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import { Handler } from 'swup';
import Plugin from '@swup/plugin';
import { updateClassNames } from './classes.js';
import { updateAttributes } from './attributes.js';

type Options = {
/** If set, only classes beginning with this string will be added/removed. */
prefix: string;
/** Additional body attributes to update, besides the classname. */
attributes: (string | RegExp)[];
};

export default class SwupBodyClassPlugin extends Plugin {
Expand All @@ -13,7 +16,8 @@ export default class SwupBodyClassPlugin extends Plugin {
requires = { swup: '>=4.6' };

defaults: Options = {
prefix: ''
prefix: '',
attributes: []
};
options: Options;

Expand All @@ -23,11 +27,14 @@ export default class SwupBodyClassPlugin extends Plugin {
}

mount() {
this.on('content:replace', this.updateBodyClass);
this.on('content:replace', this.update);
}

protected updateBodyClass: Handler<'content:replace'> = (visit) => {
const { prefix } = this.options;
protected update: Handler<'content:replace'> = (visit) => {
const { prefix, attributes } = this.options;
updateClassNames(document.body, visit.to.document!.body, { prefix });
if (attributes?.length) {
updateAttributes(document.body, visit.to.document!.body, attributes);
}
};
}
72 changes: 72 additions & 0 deletions tests/unit/attributes.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { describe, expect, it } from 'vitest';
import { updateAttributes } from '../../src/attributes.js';

function createElement(html: string): Element {
const el = document.createElement('div');
el.innerHTML = html;
return el.firstElementChild!;
};

const mergeAttributes = (currentEl: string, incomingEl: string, ...args): string => {
const current = createElement(currentEl);
const incoming = createElement(incomingEl);
updateAttributes(current, incoming, ...args);
return current.outerHTML;
};

describe('updateAttributes', () => {
describe('attributes', () => {
it('adds attributes', () => {
expect(mergeAttributes('<div></div>', '<div a="b"></div>')).toBe('<div a="b"></div>');
expect(mergeAttributes('<div></div>', '<div a="b" b="c"></div>')).toBe('<div a="b" b="c"></div>');
});

it('updates attributes', () => {
expect(mergeAttributes('<div a="b" b="c"></div>', '<div a="c" b="c"></div>')).toBe('<div a="c" b="c"></div>');
});

it('removes attributes', () => {
expect(mergeAttributes('<div a="b"></div>', '<div></div>')).toBe('<div></div>');
expect(mergeAttributes('<div a="b" b="c"></div>', '<div a="b"></div>')).toBe('<div a="b"></div>');
});

it('allows filtering attributes', () => {
expect(mergeAttributes('<div></div>', '<div a="b"></div>', [''])).toBe('<div></div>');
expect(mergeAttributes('<div></div>', '<div a="b"></div>', ['b'])).toBe('<div></div>');
expect(mergeAttributes('<div></div>', '<div b="c"></div>', ['a'])).toBe('<div></div>');
expect(mergeAttributes('<div></div>', '<div a="b" b="c"></div>', ['a'])).toBe('<div a="b"></div>');
expect(mergeAttributes('<div></div>', '<div a="b" b="c"></div>', ['b'])).toBe('<div b="c"></div>');
expect(mergeAttributes('<div></div>', '<div a="b" b="c"></div>', ['a', 'b'])).toBe('<div a="b" b="c"></div>');
expect(mergeAttributes('<div></div>', '<div a="b" abc="d" bcd="e"></div>', [/^ab/])).toBe('<div abc="d"></div>');
});

it('handles boolean attributes', () => {
expect(mergeAttributes('<div disabled></div>', '<div hidden></div>')).toBe('<div hidden=""></div>');
expect(mergeAttributes('<div></div>', '<div disabled></div>')).toBe('<div disabled=""></div>');
});
});

describe('types', () => {
const el = document.createElement('div');

it('returns nothing', () => {
expect(updateAttributes(el, el)).toBeUndefined();
});

it('only accepts elements', () => {
try {
updateAttributes(el, el);
// @ts-expect-error
updateAttributes('', el);
// @ts-expect-error
updateAttributes(el, '');
// @ts-expect-error
updateAttributes(el, 1);
// @ts-expect-error
updateAttributes(el, []);
// @ts-expect-error
updateAttributes(el);
} catch (error) {}
});
});
});
15 changes: 12 additions & 3 deletions tests/unit/plugin.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,14 +31,12 @@ describe('SwupBodyClassPlugin', () => {

it('merges user options', async () => {
const plugin = new SwupBodyClassPlugin({ prefix: 'pre-' });
expect(plugin.options).toMatchObject({ prefix: 'pre-' });
expect(plugin.options).toMatchObject({ prefix: 'pre-', attributes: [] });
});

it('updates body classname from content:replace hook handler', async () => {
const classes = await import('../../src/classes.js');
const spy = vitest.spyOn(classes, 'updateClassNames');
// classes.updateClassNames = vitest.fn()
// .mockImplementation(() => ({ removed: [], added: [] }));

await swup.hooks.call('content:replace', visit, page);

Expand All @@ -47,4 +45,15 @@ describe('SwupBodyClassPlugin', () => {
prefix: plugin.options.prefix
});
});

it('updates attributes from content:replace hook handler', async () => {
const attributes = await import('../../src/attributes.js');
const spy = vitest.spyOn(attributes, 'updateAttributes');

plugin.options.attributes = ['lang', /^aria-/];
await swup.hooks.call('content:replace', visit, page);

expect(spy).toHaveBeenCalledOnce();
expect(spy).toHaveBeenCalledWith(document.body, visit.to.document!.body, ['lang', /^aria-/]);
});
});

0 comments on commit ce914bc

Please sign in to comment.