Skip to content

Commit

Permalink
Merge pull request #14 from lroskoshin/migrate-to-new-injection
Browse files Browse the repository at this point in the history
Migrate-to-new-injection
  • Loading branch information
lroskoshin authored Nov 20, 2021
2 parents c9b7487 + c18c784 commit 65aef68
Show file tree
Hide file tree
Showing 23 changed files with 9,271 additions and 10,148 deletions.
6 changes: 4 additions & 2 deletions .eslintrc
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
{
"parser": "@typescript-eslint/parser",
"plugins": ["@typescript-eslint", "jest"],
"extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended"],
"extends": ["eslint:recommended", "plugin:@typescript-eslint/eslint-recommended", "plugin:@typescript-eslint/recommended"],
"rules": {
"eol-last": ["error", "always"],
"no-multiple-empty-lines": "error",
"semi": ["error", "always"],
"quotes": ["error", "single"]
"quotes": ["error", "single"],
"@typescript-eslint/no-unused-vars": ["error", { "argsIgnorePattern": "^_" }],
"no-console": "error"
}
}
2 changes: 2 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ jobs:
node-version: 14
- name: Install dependencies
run: npm install
- name: Typecheck
run: npm run typecheck
- name: Lint
run: npm run lint
- name: Test
Expand Down
1 change: 1 addition & 0 deletions benchmarks/template.bench.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/* eslint-disable no-console */
// This benchmark is for performance comparing querySelector vs TreeWalker
// In the browser, the result may be different

Expand Down
19,115 changes: 9,090 additions & 10,025 deletions package-lock.json

Large diffs are not rendered by default.

45 changes: 27 additions & 18 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,15 @@
"name": "horojs",
"version": "1.0.4",
"description": "A micro library for a reactive UI application.",
"keywords": ["frontend", "horo", "microlib", "dom", "ui", "templates", "reactive"],
"keywords": [
"frontend",
"horo",
"microlib",
"dom",
"ui",
"templates",
"reactive"
],
"files": [
"dist"
],
Expand All @@ -18,6 +26,7 @@
"build:es": "rollup -c rollup.config.js",
"build:umd": "rollup -c rollup.umd.config.js",
"lint": "eslint .",
"typecheck": "tsc --noEmit",
"clean-package-json": "ts-node-script utils/clean-package-json.ts"
},
"repository": {
Expand All @@ -43,25 +52,25 @@
},
"homepage": "https://github.com/lroskoshin/horo#readme",
"devDependencies": {
"@testing-library/dom": "^7.30.4",
"@testing-library/jest-dom": "^5.12.0",
"@types/benchmark": "^2.1.0",
"@types/jest": "^26.0.22",
"@typescript-eslint/eslint-plugin": "^4.22.0",
"@typescript-eslint/parser": "^4.22.0",
"@wessberg/rollup-plugin-ts": "^1.3.14",
"@testing-library/dom": "^8.11.1",
"@testing-library/jest-dom": "^5.15.0",
"@types/benchmark": "^2.1.1",
"@types/jest": "^27.0.3",
"@typescript-eslint/eslint-plugin": "^5.4.0",
"@typescript-eslint/parser": "^5.4.0",
"benchmark": "^2.1.4",
"eslint": "^7.25.0",
"eslint-plugin-jest": "^24.3.6",
"global-jsdom": "^8.1.0",
"jest": "^26.6.3",
"eslint": "^8.2.0",
"eslint-plugin-jest": "^25.2.4",
"global-jsdom": "^8.3.0",
"jest": "^27.3.1",
"jest-environment-jsdom-latest": "^26.6.2",
"jsdom": "^16.5.3",
"rollup": "^2.45.2",
"jsdom": "^18.1.0",
"rollup": "^2.60.0",
"rollup-plugin-terser": "^7.0.2",
"rxjs": "^6.6.7",
"ts-jest": "^26.5.5",
"ts-node": "^9.1.1",
"typescript": "^4.2.4"
"rollup-plugin-ts": "^2.0.4",
"rxjs": "^7.4.0",
"ts-jest": "^27.0.7",
"ts-node": "^10.4.0",
"typescript": "^4.5.2"
}
}
2 changes: 1 addition & 1 deletion rollup.config.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import ts from '@wessberg/rollup-plugin-ts';
import ts from 'rollup-plugin-ts';
import { RollupOptions } from 'rollup';
import { terser } from 'rollup-plugin-terser';
import pkg from './package.json';
Expand Down
2 changes: 1 addition & 1 deletion rollup.umd.config.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import ts from '@wessberg/rollup-plugin-ts';
import ts from 'rollup-plugin-ts';
import { RollupOptions } from 'rollup';
import { terser } from 'rollup-plugin-terser';
import pkg from './package.json';
Expand Down
4 changes: 2 additions & 2 deletions src/insertion/inject-dynamic-value.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export function injectDynamicValue(socket: Comment, insertion: DynamicInsertion)
};
const currentRange = document.createRange();

const subscription = insertion.subscribe((value: Component | string) => {
const unsubscribe = insertion((value: Component | string) => {
currentRange.setStartBefore(address.start);
currentRange.setEndAfter(address.end);
if(typeof value === 'string') {
Expand All @@ -27,7 +27,7 @@ export function injectDynamicValue(socket: Comment, insertion: DynamicInsertion)

return () => {
lastUnsubscriber();
subscription.unsubscribe();
unsubscribe?.();
};
}

Expand Down
6 changes: 3 additions & 3 deletions src/insertion/insert-attr.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
import { Subscribable, Unsubscriber } from './insertion';

export function insertAttr(socket: Element, attrName: string, insertion: string | Subscribable<string>): Unsubscriber {
export function insertAttr(socket: Element, attrName: string, insertion: string | Subscribable<string>): Unsubscriber | void {
const attr = document.createAttribute(attrName);
socket.setAttributeNode(attr);
if(typeof insertion === 'string') {
attr.value = insertion;
// eslint-disable-next-line @typescript-eslint/no-empty-function
return () => {};
} else {
const subscription = insertion.subscribe((value: string) => {
const unsubscriber = insertion((value: string) => {
attr.value = value;
});
return subscription.unsubscribe;
return unsubscriber;
}
}
31 changes: 16 additions & 15 deletions src/insertion/insertion.ts
Original file line number Diff line number Diff line change
@@ -1,43 +1,44 @@
export type Unsubscriber = () => void;

export type Unsubscribable = {
unsubscribe: Unsubscriber;
}

//TO-DO: remove unsubscriber after onDestroy hook will be added
export type Subscribable<T> = {
subscribe(listner: (value: T) => void): Unsubscribable;
(listner: (value: T) => void): Unsubscriber | void;
};

export type Subscription<T> = {
next(value: T): void;
(value: T): void;
};

type Subscriptions<T> = T extends infer K ? Subscription<K> : never;

export interface Component {
unsubscribe(): void;
fragment: DocumentFragment;
}
export type StaticInsertion = string | Component;
export type DynamicInsertion = Subscribable<string | Component>;
export type ValueInsertion = DynamicInsertion | StaticInsertion;
export type Instertion = Subscription<Event> | ValueInsertion;
export type Instertion = Subscriptions<GlobalEventHandlersEventMap[keyof GlobalEventHandlersEventMap]> | ValueInsertion;
// TO-DO: Optimize type guarding
export function isStaticInsertion(insertion: Instertion): insertion is StaticInsertion {
return typeof insertion === 'string' || 'fragment' in insertion;
}
export function isDynamicInsertion(insertion: Instertion): insertion is StaticInsertion {
return typeof insertion !== 'string' && 'subscribe' in insertion && typeof insertion.subscribe === 'function';

export function isDynamicInsertion(insertion: Instertion): insertion is DynamicInsertion {
return typeof insertion === 'function';
}

export function ensureValueInsertion(instertion: Instertion): ValueInsertion {
if(isStaticInsertion(instertion) || isDynamicInsertion(instertion)) {
return instertion;
}
}

throw new Error('The passed value is not Instertable.');
}

export function ensureSubscription(instertion: Instertion): Subscription<unknown> {
if(typeof instertion !== 'string' && 'next' in instertion && typeof instertion.next === 'function') {
return instertion;
}
export function ensureSubscription(instertion: Instertion): Subscription<Event> {
if(typeof instertion === 'function') {
return instertion as Subscription<Event> ;
}

throw new Error('The passed value is not a Subscription.');
}
2 changes: 1 addition & 1 deletion src/listen-event.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { Subscription, Unsubscriber } from './insertion/insertion';

export function listenEvent(element: Element, eventName: string, subscription: Subscription<Event>): Unsubscriber {
const handler = (event: Event) => {
subscription.next(event);
subscription(event);
};
element.addEventListener(eventName, handler);
return () => element.removeEventListener(eventName, handler);
Expand Down
14 changes: 12 additions & 2 deletions src/reactivity/make-it-reactive.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
import { attrsPrefixLength, eventHandlingPrefixLength, insertionPrefixLength, isAttrsInstruction, isEventHandlingInstruction, isInsertionInsctruction } from '../instruction';
import {
attrsPrefixLength,
eventHandlingPrefixLength,
insertionPrefixLength,
isAttrsInstruction,
isEventHandlingInstruction,
isInsertionInsctruction
} from '../instruction';
import { insertValue } from '../insertion/insert-value';
import { ensureSubscription, ensureValueInsertion, Instertion, Subscribable, Unsubscriber } from '../insertion/insertion';
import { listenEvent } from '../listen-event';
Expand Down Expand Up @@ -26,7 +33,10 @@ export function makeItReactive(fragment: DocumentFragment, insertions: ArrayWith
} else if (isAttrsInstruction(instruction)) {
const [attrName, insertionIndex] = instruction.substring(attrsPrefixLength).split(':');
const insertion = ensureValueInsertion(insertions[insertionIndex]);
unsubscribes.push(insertAttr(socket.nextElementSibling as Element, attrName, insertion as string | Subscribable<string>));
const unsubscribe = insertAttr(socket.nextElementSibling as Element, attrName, insertion as string | Subscribable<string>);
if(unsubscribe) {
unsubscribes.push(unsubscribe);
}
}
}
return () => {
Expand Down
1 change: 1 addition & 0 deletions src/utils/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export { mergeComponents } from './merge-components';
export { state } from './state';
22 changes: 22 additions & 0 deletions src/utils/state.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
type Dispatcher<Value> = {
(value: Value): void
}

type State<Value> = {
(cb: (value: Value) => void): void;
}

export function state<Value>(value: Value): [State<Value>, Dispatcher<Value>] {
let currentValue = value;
const linsteners: Array<(value: Value) => void> = [];
return [
(cb) => {
cb(currentValue);
linsteners.push(cb);
},
(newValue: Value) => {
currentValue = newValue;
linsteners.forEach((cb) => cb(currentValue));
}
];
}
25 changes: 11 additions & 14 deletions test/attrs.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,29 +2,26 @@
* @jest-environment jsdom-latest
*/
import { getByTestId } from '@testing-library/dom';
import { ReplaySubject, Subscription } from 'rxjs';
import { horo } from '../src/horo';
import { Component } from '../src/insertion/insertion';
import { state } from '../src/utils';

describe('Reactive Insert RxJS', () => {
let component: Component;
const classSubject = new ReplaySubject<string>();

let spy: jasmine.Spy;
const originalSubscribe = classSubject.subscribe.bind(classSubject);
const newSubscribe = (...args: never[]): Subscription => {
const subscription = originalSubscribe(...args);
spy = spyOn(subscription, 'unsubscribe');
return subscription;
const [classes, setClasses] = state('foo');

const stub: jest.Mock<void, []> = jest.fn();
const newSubscribe = (cb: (v: string) => void) => {
classes(cb);
return stub;
};

classSubject.subscribe = newSubscribe;
classSubject.next('foo');
setClasses('foo');
const element = document.createElement('div');

beforeAll(() => {
component = horo`
<div data-testid="reactive" class="${classSubject}">
<div data-testid="reactive" class="${newSubscribe}">
Hello World
</div>
<div data-testid="static" class="${'baz'}">
Expand All @@ -39,7 +36,7 @@ describe('Reactive Insert RxJS', () => {
});

it('Update Text', () => {
classSubject.next('bar');
setClasses('bar');
expect(getByTestId(element, 'reactive')).toHaveClass('bar');
});

Expand All @@ -49,6 +46,6 @@ describe('Reactive Insert RxJS', () => {

it('Unsubscribe', () => {
component.unsubscribe();
expect(spy).toHaveBeenCalled();
expect(stub).toHaveBeenCalled();
});
});
15 changes: 8 additions & 7 deletions test/component/test-application.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { Observable, ReplaySubject } from 'rxjs';
import { horo } from '../../src/horo';
import { Component } from '../../src/insertion/insertion';
import { Component, Subscribable } from '../../src/insertion/insertion';

export function mount(root: Element): void {
const component = horo`
Expand All @@ -11,10 +10,12 @@ export function mount(root: Element): void {
root.appendChild(component.fragment);
}

function HelloWorldComponent(): Observable<Component> {
const component = new ReplaySubject<Component>();
component.next(horo`
function HelloWorldComponent(): Subscribable<Component> {
const component = horo`
<span> Hello World! <span>
`);
return component;
`;

return (cb) => {
cb(component);
};
}
22 changes: 7 additions & 15 deletions test/condition.spec.ts
Original file line number Diff line number Diff line change
@@ -1,33 +1,25 @@
/**
* @jest-environment jsdom-latest
*/
import { Subject } from 'rxjs';
import { map, startWith } from 'rxjs/operators';
import { horo } from '../src/horo';
import {
getByTestId,
fireEvent,
} from '@testing-library/dom';
import { state } from '../src/utils';

describe('Event Handling RxJS', () => {
const checked = new Subject<Event>();
const text = checked.pipe(
map((event: Event): string => {
if((event.target as HTMLInputElement).checked) {
return 'True';
} else {
return 'Flase';
}
}),
startWith('False')
);
const [checked, setChecked] = state('False');
let current: string;
checked((v) => current = v);
const element = document.createElement('div');
const check = () => setChecked(current === 'False' ? 'True' : 'False');

beforeAll(() => {
const component = horo`
<div>
<input type="checkbox" @click=${checked} data-testid="checkbox"></input>
<span>${text}</span>
<input type="checkbox" @click=${check} data-testid="checkbox"></input>
<span>${checked}</span>
</div>
`;
element.appendChild(component.fragment);
Expand Down
Loading

0 comments on commit 65aef68

Please sign in to comment.