Skip to content

Commit

Permalink
Merge pull request #7 from Tim-mhn/feat/sourceless-reactivity
Browse files Browse the repository at this point in the history
feat(reactivity): enabled subscribing to sources without specifying s…
  • Loading branch information
Tim-mhn authored Aug 14, 2024
2 parents 1c24db6 + fe49d5e commit 536aaa8
Show file tree
Hide file tree
Showing 17 changed files with 116 additions and 63 deletions.
23 changes: 11 additions & 12 deletions .github/workflows/node.js.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,12 @@ name: Node.js CI

on:
push:
branches: [ "main" ]
branches: ['main']
pull_request:
branches: [ "main" ]
branches: ['main']

jobs:
build:

runs-on: ubuntu-latest

strategy:
Expand All @@ -20,12 +19,12 @@ jobs:
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/

steps:
- run: corepack enable
- uses: actions/checkout@v4
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
cache: 'yarn'
- run: yarn install
- run: cd packages/core && yarn run test:ci
- run: corepack enable
- uses: actions/checkout@v4
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
cache: 'yarn'
- run: yarn install
- run: cd packages/core && yarn install && yarn run test:ci
9 changes: 6 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,11 +54,11 @@ count.update(3);
console.log(count.value); // 3
```

Create computed values with `computed`. You have to pass the array of reactive values it depends on.
Create computed values with `computed`.

```
const count = reactive(1);
const double = computed(() => count.value * 2, [count])
const double = computed(() => count.value * 2)
```

### Component
Expand Down Expand Up @@ -99,7 +99,9 @@ const DemoCard = component(() => {
```

### Control-flow (for-loop & if/else)

#### For loop

To render a list of components, use the `<For />` component

```
Expand All @@ -115,6 +117,7 @@ const FruitsList = component(() => {
```

#### If/Else

To conditionnally render a component or a fallback, use the `<Show />` component (heaviliy inspired from Solid's [Show](https://docs.solidjs.com/concepts/control-flow/conditional-rendering)

```
Expand All @@ -124,7 +127,7 @@ import { bool } from "tinaf/reactive";
export const ShowExample = component(() => {
const [isHappy, toggleMood] = bool(true)
return <div>
return <div>
<Show when={condition} fallback={<div> sad! </div>} >
<div> happy ! </div>
</Show>
Expand Down
6 changes: 3 additions & 3 deletions docs/routing.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ const Bar = component(() => "Bar")
}]
```

### 2. Create the router
### 2. Create the router

```
import { createRouter } from 'tinaf/router';
Expand Down Expand Up @@ -166,10 +166,10 @@ const ProductPage = component(() => {
const router = injectRouter();
const productId = computed(
() => router.route.value.params.productId, [router.route]
() => router.route.value.params.productId,
);
const product = computed(() => getProduct(productId.value), [productId]);
const product = computed(() => getProduct(productId.value));
const { title } = toReactiveProps(product);
Expand Down
8 changes: 4 additions & 4 deletions packages/core/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ Create computed values with `computed`. You have to pass the array of reactive v

```
const count = reactive(1);
const double = computed(() => count.value * 2, [count])
const double = computed(() => count.value * 2)
```

### Component
Expand Down Expand Up @@ -125,7 +125,7 @@ const Example = component(() => {
const [active, _] = bool(true);
const backgroundClass = computed(() => active.value ? 'bg-blue-300' : 'bg-red-300', [active])
const backgroundClass = computed(() => active.value ? 'bg-blue-300' : 'bg-red-300')
return div('Hello').addClasses([backgroundClass, 'text-sm', 'border', 'border-slate-300'])
})
Expand All @@ -138,7 +138,7 @@ const Example = component(() => {
const [active, _] = bool(true);
const backgroundColor = computed(() => active.value ? 'blue' : 'red', [active])
const backgroundColor = computed(() => active.value ? 'blue' : 'red')
return div('Hello').addStyles({
background: backgroundColor,
Expand Down Expand Up @@ -176,7 +176,7 @@ const Card = component<{ title: string; subtitle: string}>(({ title, subtitle})
const Button = component(() => {
const [active, toggleActive] = bool(true);
const buttonText = computed(() => active.value ? 'Deactivate' : 'Activate', [active]);
const buttonText = computed(() => active.value ? 'Deactivate' : 'Activate');
const buttonClasses = computed(() => active.value ? 'bg-green-300 border-green-300' : 'bg-slate-300 border-slate-400')
return button(buttonText)
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/component/SimpleSwitch.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ describe('Switch/Match', () => {
);
});

const { children, cmp } = fakeMount(TestComponent);
const { children } = fakeMount(TestComponent);
expect(children).toEqual(['a']);
});

Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/dom/classes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ function transformRecordIntoReactiveClassesArray(

if (!isReactive(condition)) return toValue(condition) ? className : '';

return computed(() => (condition.value ? className : ''), [condition]);
return computed(() => (condition.value ? className : ''));
});
}

Expand Down
4 changes: 2 additions & 2 deletions packages/core/src/reactive/boolean.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export class BooleanReactive extends Reactive<boolean> {
}

public not() {
return computed(() => !this.value, [this]);
return computed(() => !this.value);
}
}

Expand All @@ -22,7 +22,7 @@ export function bool(initialValue: boolean): [Reactive<boolean>, () => void] {

export function not(condition: MaybeReactive<boolean>) {
if (isReactive(condition)) {
return computed(() => !condition.value, [condition]);
return computed(() => !condition.value);
}

return !condition;
Expand Down
30 changes: 20 additions & 10 deletions packages/core/src/reactive/computed.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ describe('Computed', () => {
it('emits a value change when its single source changes', () => {
const source = reactive(1);

const double = computed(() => source.value * 2, [source]);
const double = computed(() => source.value * 2);

let emitted = false;
double.valueChanges$.pipe(take(1)).subscribe(() => {
Expand All @@ -17,20 +17,12 @@ describe('Computed', () => {
expect(emitted).toBeTruthy();
});

it('throws an error if no sources are passed', () => {
const source = reactive(1);

const createComputedValueFn = () => computed(() => source.value * 2, []);

expect(createComputedValueFn).toThrow();
});

it('emits a value whenever one of the sources changes', () => {
const a = reactive(1);
const b = reactive(1);
const c = reactive(1);

const sum = computed(() => a.value + b.value + c.value, [a, b, c]);
const sum = computed(() => a.value + b.value + c.value);

const emissions: number[] = [];

Expand All @@ -48,4 +40,22 @@ describe('Computed', () => {
// Emission #3: 2 + 5 + 10 = 17
expect(emissions).toEqual([4, 8, 17]);
});

it('emits a value when it has a Computed has a single source and the source changes ', () => {
const source = reactive(1);
const double = computed(() => source.value * 2);

const quadruple = computed(() => double.value * 2);

let emitted = false;
let val: number = 0;
quadruple.valueChanges$.pipe(take(1)).subscribe((emittedValue) => {
emitted = true;
val = emittedValue;
});
source.update(2);

expect(emitted).toBeTruthy();
expect(val).toEqual(8);
});
});
37 changes: 37 additions & 0 deletions packages/core/src/reactive/effect.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { describe, expect, it, vi } from 'vitest';
import { reactive } from './reactive';
import { effect } from './effect';

describe('effect', () => {
it('executes the callback function when the value changes', () => {
const source = reactive('foo');

const callback = vi.fn();

effect(() => {
callback();
}, [source]);

expect(callback).not.toHaveBeenCalled();

source.update('bar');

expect(callback).toHaveBeenCalled();
});

it('stops watching the sources once we call the unsubscribe method', () => {
const source = reactive('foo');

const callback = vi.fn();

const unsubscribe = effect(() => {
callback();
}, [source]);

unsubscribe();

source.update('bar');

expect(callback).not.toHaveBeenCalled();
});
});
8 changes: 8 additions & 0 deletions packages/core/src/reactive/effect.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import type { ReactiveValue } from './reactive';
import { watchAllSources } from './watch';

export function effect<T>(callbackFn: () => void, sources: ReactiveValue<T>[]) {
const sub = watchAllSources(sources).subscribe(() => callbackFn());

return () => sub.unsubscribe();
}
31 changes: 12 additions & 19 deletions packages/core/src/reactive/reactive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,13 @@ import {
skip,
startWith,
} from 'rxjs';
import type { MaybeReactive } from './types';
import { isReactive } from './toValue';
export interface ReactiveValue<T> {
value: T;
valueChanges$: Observable<T>;
}

let observables: ReactiveValue<any>[] = [];

export class Reactive<T> implements ReactiveValue<T> {
private _value: T;
constructor(initialValue: T) {
Expand All @@ -30,6 +30,7 @@ export class Reactive<T> implements ReactiveValue<T> {
}

get value() {
observables.push(this);
return this._value;
}
}
Expand All @@ -42,17 +43,19 @@ export function reactive<T>(initialValue: T) {
class Computed<T> implements ReactiveValue<T> {
valueChanges$: Observable<T>;

constructor(public getterFn: () => T, sources: ReactiveValue<any>[]) {
if (sources.length === 0)
throw new Error(
'Computed value was created without any source. Please include at least 1 source'
);
constructor(public getterFn: () => T) {
getterFn();

const sources = [...observables];

this.valueChanges$ = combineLatest(
sources.map((s) => s.valueChanges$.pipe(startWith('')))
).pipe(
skip(1),
map(() => getterFn())
);

observables = [];
}

get value() {
Expand Down Expand Up @@ -93,16 +96,6 @@ export function inputReactive<T extends string | number>(initialValue: T) {
return new InputReactive(initialValue);
}

export function computed<T>(getterFn: () => T, sources: ReactiveValue<any>[]) {
return new Computed(getterFn, sources);
}

export function maybeComputed<T>(
getterFn: () => T,
sources: MaybeReactive<any>[]
) {
const reactiveSources = sources.filter(isReactive);
if (reactiveSources.length === 0) return getterFn();

return computed(getterFn, reactiveSources);
export function computed<T>(getterFn: () => T) {
return new Computed(getterFn);
}
4 changes: 2 additions & 2 deletions packages/core/src/reactive/toReactiveProps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ function recursivelyBuildReactiveProps<T extends object>(
const v = objValue[key];

if (typeof v === 'object' && v !== null) {
const localReactive = computed(() => (toValue(obj) as T)[key], [obj]);
const localReactive = computed(() => (toValue(obj) as T)[key]);
const nestedReactiveProps = toReactiveProps(localReactive, {
deep: true,
});
Expand Down Expand Up @@ -72,7 +72,7 @@ function buildSimpleReactiveProps<T extends object>(
const reactiveProps: Partial<O<T>> = {};

for (const key of objectKeys(toValue(obj))) {
const reactiveProp = computed(() => toValue(obj)[key], [obj]);
const reactiveProp = computed(() => toValue(obj)[key]);

reactiveProps[key] = reactiveProp;
}
Expand Down
5 changes: 4 additions & 1 deletion packages/core/src/test-utils/fake-app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { vi } from 'vitest';
import type { VComponent } from '../component';
import { FakeWindow } from './fake-window';
import { provideFakeTinafApp } from './inject-app.mock';
import { buildTestRouter } from '../router/test-utils';

// // FIXME: use one version of fakeApp
const createFakeApp = () => createApp(() => '' as any, {} as any, {} as any);
Expand All @@ -19,7 +20,9 @@ export const createMockDocument = (): IDocument => ({
});

const createFakeTextNode = (): Text => ({});
export const setupFakeApp = ({ router }: { router: Router }) => {
export const setupFakeApp = ({
router = buildTestRouter([]),
}: { router?: Router } = {}) => {
const fakeApp = createFakeApp();
fakeApp.provide(ROUTER_PROVIDER_KEY, router);
provideFakeTinafApp(fakeApp);
Expand Down
3 changes: 1 addition & 2 deletions packages/create-tinaf/demo/Home/components/Card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import { button, div } from 'tinaf/dom';
import {
computed,
inputReactive,
maybeComputed,
not,
reactive,
toValue,
Expand All @@ -16,7 +15,7 @@ type CardProps = {
theme: 'light' | 'dark';
};
const Card = component<CardProps>(({ title, description, theme }) => {
const isLight = maybeComputed(() => toValue(theme) === 'light', [theme]);
const isLight = computed(() => toValue(theme) === 'light');
const isDark = not(isLight);

return div(
Expand Down
Loading

0 comments on commit 536aaa8

Please sign in to comment.