Skip to content

Commit

Permalink
Merge pull request #58 from swup/feat/modify-rules
Browse files Browse the repository at this point in the history
  • Loading branch information
hirasso authored Nov 10, 2023
2 parents 9d1bf07 + 07605f3 commit 12cbc60
Show file tree
Hide file tree
Showing 7 changed files with 187 additions and 37 deletions.
79 changes: 52 additions & 27 deletions src/SwupFragmentPlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@ import {
handlePageView,
cleanupFragmentElements,
getFirstMatchingRule,
getFragmentVisitContainers
getFragmentVisitContainers,
cloneRules
} from './inc/functions.js';
import type { Options, Route, FragmentVisit } from './inc/defs.js';
import type { Options, Rule, Route, FragmentVisit } from './inc/defs.js';
import * as handlers from './inc/handlers.js';
import { __DEV__ } from './inc/env.js';

Expand All @@ -18,15 +19,19 @@ type InitOptions = RequireKeys<Options, 'rules'>;
* The main plugin class
*/
export default class SwupFragmentPlugin extends PluginBase {
name = 'SwupFragmentPlugin';
readonly name = 'SwupFragmentPlugin';
readonly requires = { swup: '>=4' };

requires = { swup: '>=4' };
protected _rawRules: Rule[] = [];
protected _parsedRules: ParsedRule[] = [];

rules: ParsedRule[] = [];
get parsedRules() {
return this._parsedRules;
}

options: Options;

defaults: Options = {
protected defaults: Options = {
rules: [],
debug: false
};
Expand All @@ -39,11 +44,7 @@ export default class SwupFragmentPlugin extends PluginBase {
*/
constructor(options: InitOptions) {
super();

this.options = { ...this.defaults, ...options };
if (__DEV__) {
if (this.options.debug) this.logger = new Logger();
}
}

/**
Expand All @@ -52,18 +53,10 @@ export default class SwupFragmentPlugin extends PluginBase {
mount() {
const swup = this.swup;

this.rules = this.options.rules.map(({ from, to, containers, name, scroll, focus }) => {
return new ParsedRule({
from,
to,
containers,
name,
scroll,
focus,
logger: this.logger,
swup: this.swup
});
});
this.setRules(this.options.rules);
if (__DEV__) {
if (this.options.debug) this.logger = new Logger();
}

this.before('link:self', handlers.onLinkToSelf);
this.on('visit:start', handlers.onVisitStart);
Expand All @@ -73,8 +66,6 @@ export default class SwupFragmentPlugin extends PluginBase {
this.on('content:replace', handlers.onContentReplace);
this.on('visit:end', handlers.onVisitEnd);

swup.getFragmentVisit = this.getFragmentVisit.bind(this);

if (__DEV__) {
this.logger?.warnIf(
!swup.options.cache,
Expand All @@ -90,16 +81,50 @@ export default class SwupFragmentPlugin extends PluginBase {
*/
unmount() {
super.unmount();
this.swup.getFragmentVisit = undefined;
this.rules = [];
cleanupFragmentElements();
}

setRules(rules: Rule[]) {
this._rawRules = cloneRules(rules);
this._parsedRules = rules.map((rule) => this.parseRule(rule));
if (__DEV__) this.logger?.log('Updated fragment rules', this.getRules());
}

getRules() {
return cloneRules(this._rawRules);
}

prependRule(rule: Rule) {
this.setRules([rule, ...this.getRules()]);
}

appendRule(rule: Rule) {
this.setRules([...this.getRules(), rule]);
}

/**
* Add a fragment rule
* @param {Rule} rule The rule options
* @param {'start' | 'end'} at Should the rule be added to the beginning or end of the existing rules?
*/
parseRule({ from, to, containers, name, scroll, focus }: Rule): ParsedRule {
return new ParsedRule({
from,
to,
containers,
name,
scroll,
focus,
logger: this.logger,
swup: this.swup
});
}

/**
* Get the fragment visit object for a given route
*/
getFragmentVisit(route: Route): FragmentVisit | undefined {
const rule = getFirstMatchingRule(route, this.rules);
const rule = getFirstMatchingRule(route, this.parsedRules);

// Bail early if no rule matched
if (!rule) return;
Expand Down
20 changes: 17 additions & 3 deletions src/inc/functions.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Location } from 'swup';
import type { Swup, Visit, VisitScroll } from 'swup';
import type { default as FragmentPlugin } from '../SwupFragmentPlugin.js';
import type { Route, FragmentVisit, FragmentElement } from './defs.js';
import type { Route, Rule, FragmentVisit, FragmentElement } from './defs.js';
import type ParsedRule from './ParsedRule.js';
import Logger, { highlight } from './Logger.js';

Expand Down Expand Up @@ -82,10 +82,10 @@ function handleLinksToFragments({ logger, swup }: FragmentPlugin): void {
/**
* Adds attributes and properties to fragment elements
*/
function prepareFragmentElements({ rules, swup, logger }: FragmentPlugin): void {
function prepareFragmentElements({ parsedRules, swup, logger }: FragmentPlugin): void {
const currentUrl = swup.getCurrentUrl();

rules
parsedRules
.filter((rule) => rule.matchesFrom(currentUrl) || rule.matchesTo(currentUrl))
.forEach((rule) => {
rule.containers.forEach((selector) => {
Expand Down Expand Up @@ -379,3 +379,17 @@ export function queryFragmentElement(
}
return;
}

/**
* Clone fragment rules (replacement for `structuredClone`)
*/
export function cloneRules(rules: Rule[]): Rule[] {
if (!Array.isArray(rules)) throw new Error(`cloneRules() expects an array of rules`);

return rules.map((rule) => ({
...rule,
from: Array.isArray(rule.from) ? [...rule.from] : rule.from,
to: Array.isArray(rule.to) ? [...rule.to] : rule.to,
containers: [...rule.containers]
}));
}
2 changes: 1 addition & 1 deletion src/inc/handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export const onLinkToSelf: Handler<'link:self'> = function (this: FragmentPlugin
const route = getRoute(visit);
if (!route) return;

const rule = getFirstMatchingRule(route, this.rules);
const rule = getFirstMatchingRule(route, this.parsedRules);

if (rule) visit.scroll.reset = false;
};
Expand Down
4 changes: 4 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ export type { Options, Rule, FragmentElement, FragmentVisit } from './inc/defs.j
declare module 'swup' {
export interface Swup {
getFragmentVisit?: FragmentPlugin['getFragmentVisit'];
getFragmentRules?: FragmentPlugin['getRules'];
setFragmentRules?: FragmentPlugin['setRules'];
prependFragmentRule?: FragmentPlugin['prependRule'];
appendFragmentRule?: FragmentPlugin['appendRule'];
}
export interface Visit {
fragmentVisit?: FragmentVisit;
Expand Down
26 changes: 26 additions & 0 deletions tests/vitest/cloneRules.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { describe, expect, it } from 'vitest';
import { cloneRules } from '../../src/inc/functions.js';

describe('cloneRules()', () => {
it('should clone rules', () => {
const original = [
{ from: 'foo', to: 'bar', containers: ['#foobar'] },
{ from: ['/', 'baz'], to: ['/', 'bat'], containers: ['#bazbat'] }
];
/** test equality with a reference */
const reference = original;
expect(original === reference).toEqual(true);

/** test shallow copy */
const shallowCopy = { ...original };
expect(original === shallowCopy).toEqual(false);
expect(original[0].containers === shallowCopy[0].containers).toEqual(true);
expect(original[1].from === shallowCopy[1].from).toEqual(true);

/** test deep clone */
const clone = cloneRules(original);
expect(original === clone).toEqual(false);
expect(original[0].containers === clone[0].containers).toEqual(false);
expect(original[1].from === clone[1].from).toEqual(false);
});
});
8 changes: 2 additions & 6 deletions tests/vitest/getFragmentVisit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,18 +19,14 @@ describe('getFragmentVisit()', () => {
vi.restoreAllMocks();
});
it('should be callable as public API', () => {
const fromPlugin = fragmentPlugin.getFragmentVisit({ from: '/page-1/', to: '/page-2/' });
const fromSwup = fragmentPlugin.swup.getFragmentVisit?.({ from: '/page-1/',to: '/page-2/' }); // prettier-ignore
const fragmentVisit = fragmentPlugin.getFragmentVisit({ from: '/page-1/', to: '/page-2/' });

expect(fromPlugin).toEqual({
expect(fragmentVisit).toEqual({
containers: ['#fragment-1'],
name: undefined,
scroll: false,
focus: undefined
});

/** make sure the method exists on swup as well */
expect(fromPlugin).toEqual(fromSwup);
});

it('should return undefined if there is no matching rule', () => {
Expand Down
85 changes: 85 additions & 0 deletions tests/vitest/modifyRules.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest';
import { getMountedPluginInstance, spyOnConsole } from './inc/helpers.js';

const defaultRule = {
from: '/page-1/',
to: '/page-2/',
containers: ['#default']
};
const fragmentPlugin = getMountedPluginInstance({
rules: [defaultRule]
});

describe('modify fragment rules', () => {
beforeEach(() => {
spyOnConsole();
/** Reset the rules */
fragmentPlugin.setRules([defaultRule]);
});
afterEach(() => {
vi.restoreAllMocks();
});

it('should return unparsed raw rules', () => {
expect(fragmentPlugin.getRules()).toEqual([defaultRule]);
});

it('should provide API methods on the swup instance', () => {
expect(fragmentPlugin.getRules).toBeTypeOf('function');
expect(fragmentPlugin.setRules).toBeTypeOf('function');
expect(fragmentPlugin.prependRule).toBeTypeOf('function');
expect(fragmentPlugin.appendRule).toBeTypeOf('function');
});

it('should provide access to prependRule() and appendRule()', () => {
const prependRule = {
from: '/corge/',
to: '/grault/',
containers: ['#corgegrault']
};
const appendRule = {
from: '/garply/',
to: '/waldo/',
containers: ['#garplywaldo']
};

fragmentPlugin.prependRule(prependRule);
fragmentPlugin.appendRule(appendRule);

expect(fragmentPlugin.getRules()).toEqual([prependRule, defaultRule, appendRule]);
});

it('should provide access to getRules() and setRules()', () => {
/** Add a rule using the plugin's API, *after* the existing rules */
const appendRule = {
from: '/foo/',
to: '/bar/',
containers: ['#foobar'],
name: 'from-plugin'
};
fragmentPlugin.setRules([...fragmentPlugin.getRules(), appendRule]);

/** Add a rule using swup's API, *before* the existing rules */
const prependRule = {
from: '/baz/',
to: '/bat/',
containers: ['#bazbat'],
name: 'from-swup'
};
fragmentPlugin.setRules([prependRule, ...fragmentPlugin.getRules()]);

expect(fragmentPlugin.getRules()).toEqual([prependRule, defaultRule, appendRule]);
});

it('should remove rules', () => {
fragmentPlugin.appendRule({
from: '/foo/',
to: '/bar/',
containers: ['#foobar'],
name: 'remove-me'
});
const rules = fragmentPlugin.getRules();
fragmentPlugin.setRules(rules.filter((rule) => rule.name !== 'remove-me'));
expect(fragmentPlugin.getRules()).toEqual([defaultRule]);
});
});

0 comments on commit 12cbc60

Please sign in to comment.