What about Svelte? #144
Replies: 4 comments 2 replies
-
@jamesgpearce sorry for the tag, do you know about it? |
Beta Was this translation helpful? Give feedback.
-
Hi @frederikhors. Yes. I am tinkering with a small project where I use Svelte (4.2) + Vite (no Sveltekit) and TinyBase. Here's a simplified example of my setup: <!-- App.svelte -->
<script setup lang="ts">
import { db, getDbDogsByAge } from "../lib/db";
let dogs = [];
$: dogs = getDbDogsByAge();
db.addTableListener("dogs", () => {
dogs = getDbDogsByAge();
});
function addDog() {
db.setRow("dogs", "0004", {
name: "New Boi",
age: 0,
});
}
</script>
<h1>Dog List</h1>
<button on:click={addDog}>add new dog</button>
{#each dogs as dog}
<div>{dog.name}, of age {dog.age}</div>
{/each} // /lib/db.ts
import { createQueries, createStore } from "tinybase/with-schemas";
export const db = createStore().setTablesSchema({
dogs: {
name: { type: "string" },
age: { type: "number" },
},
});
db.setTables({
dogs: {
"0001": { name: "Fluffy", age: 3 },
"0002": { name: "Bingo", age: 10 },
"0003": { name: "Warmonger", age: 1 },
},
});
const queries = createQueries(db);
export const getDbDogsByAge = () => {
queries.setQueryDefinition("getDogs", "dogs", ({ select }) => {
select("name");
select("age");
});
return queries
.getResultSortedRowIds("getDogs", "age", true)
.map((rowId) => queries.getResultRow("dogs", rowId));
}; And it just works! Reactivity and all. The code above is a bit silly but you can separate things (schema, initialization, queries) in multiple files and maybe write wrappers to improve the listener syntax. I am even using typescript and it works well. Currently queries have a minimal type support, meaning that if I want autocompletion on my query's result I have to write by hand the result's type and cast it. Not ideal, but I can reuse the type I wrote when typing the db schema. A bit sad this is missing but it's super understandable... auto-typing those queries doesn't look easy. I am also adding persistence and all seems going smoothly. So I would suggest you to give it a try 👍 |
Beta Was this translation helpful? Give feedback.
-
I've been playing with Svelte myself here. I'm writing some store helpers to listen, I dont have all of the things covered, but what I have I'll post here, the rest is just more of the same: import { readable, writable } from 'svelte/store';
import { getContext, onDestroy } from 'svelte';
import type { Id, Relationships, Row, Store, Value } from 'tinybase';
export function getStore(): Store | undefined {
const { store }: { store: Store } = getContext('tinybase') || {};
return store;
}
export function getStoreForce(): Store {
const store = getStore();
if (!store) throw new Error('Store not found in context');
return store;
}
export function getRelationships() {
const { relationships }: { relationships: Relationships } = getContext('tinybase') || {};
return relationships;
}
export function useValue(id: Id, defaultValue?: Value) {
const store = getStoreForce();
const { subscribe, set } = writable(store.getValue(id) || defaultValue, () => {
const listener = store.addValueListener(id, (store, valueId, newValue) => {
set(newValue)
})
return () => store.delListener(listener)
});
return {
subscribe,
set: (x: Value) => {
store.setValue(id, x)
}
}
}
export function useTable(tableId: Id) {
const store = getStoreForce();
return readable(store.getTable(tableId), (set) => {
const listener = store.addTableListener(tableId, (store, tableId) => {
set(store.getTable(tableId));
})
return () => store.delListener(listener)
});
}
export function useRowIds(tableId: Id) {
const store = getStoreForce();
return readable(store.getRowIds(tableId), (set) => {
const listener = store.addRowIdsListener(tableId, (store, tableId) => {
set(store.getRowIds(tableId));
})
return () => store.delListener(listener)
});
}
export function useRow(tableId: Id, rowId: Id) {
const store = getStoreForce();
const { subscribe, set } = writable(store.getRow(tableId, rowId), () => {
const listener = store.addRowListener(tableId, rowId, () => {
set(store.getRow(tableId, rowId));
})
return () => store.delListener(listener)
});
return {
subscribe,
set: (x: Row) => {
store.setRow(tableId, rowId, x)
}
}
}
export function useCell(tableId: Id, rowId: Id, cellId: Id, defaultValue?: Value) {
const store = getStoreForce();
const { subscribe, set } = writable(store.getCell(tableId, rowId, cellId) || defaultValue, () => {
const listener = store.addCellListener(tableId, rowId, cellId, (store, tableId, rowId, cellId, newCell) => {
set(newCell)
})
return () => store.delListener(listener)
});
return {
subscribe,
set: (x: Value) => {
store.setCell(tableId, rowId, cellId, x)
}
}
}
export function useRelationshipLocalRowIds(relationshipId: Id, remoteRowId: Id) {
const relationships = getRelationships()
return readable(relationships.getLocalRowIds(relationshipId, remoteRowId), (set) => {
const listener = relationships.addLocalRowIdsListener(relationshipId, remoteRowId, (relationships, relationshipId, remoteRowId) => {
set(relationships.getLocalRowIds(relationshipId, remoteRowId));
})
return () => relationships.delListener(listener)
});
} So you get the store reactivity with them. |
Beta Was this translation helpful? Give feedback.
-
Was just playing around with TinyBase and Svelte 5. Maybe this is interesting if anyone wants to use TinyBase with the new runes. First I created a store manager that handles the initialization and also exports some helper types: import {
createMergeableStore,
createRelationships,
type MergeableStore,
type Relationships,
type TablesSchema,
type ValuesSchema
} from 'tinybase/with-schemas';
const tablesSchema = {
pets: {
name: { type: 'string' }
},
owners: {
firstName: { type: 'string' },
lastName: { type: 'string' }
}
} satisfies TablesSchema;
const valuesSchema = {} satisfies ValuesSchema;
export type Schemas = [typeof tablesSchema, typeof valuesSchema];
export type Store = MergeableStore<Schemas>;
export type TableId = keyof typeof tablesSchema;
export type CellIds = { [tableId in TableId]: keyof (typeof tablesSchema)[tableId] };
export class StoreManager {
store: MergeableStore<Schemas> | null = $state(null);
relationships: Relationships<Schemas> | null = $state(null);
async init() {
this.store = createMergeableStore().setTablesSchema(tablesSchema).setValuesSchema(valuesSchema);
this.relationships = createRelationships(this.store);
// possibly init persister and synchronizer
}
}
const manager = new StoreManager();
export default manager; And then I made some hooks: import type { CellIds, Schemas, Store, TableId } from '$lib/store.svelte';
import type { Id, Row, Table } from 'tinybase/with-schemas';
function createTableHandler<T extends TableId>(
store: Store,
tableId: T
): ProxyHandler<Table<Schemas[0], T, false>> {
return {
get(target, prop, receiver) {
const val = Reflect.get(target, prop, receiver);
if (!val) return val;
// nested proxies :D
const handler = createRowHandler(store, tableId, prop as Id);
return new Proxy(val, handler);
},
set(_, prop, value) {
store.setRow(tableId, prop as Id, value);
return true;
}
};
}
function createRowHandler<T extends TableId>(
store: Store,
tableId: T,
rowId: Id
): ProxyHandler<Row<Schemas[0], T, false>> {
return {
get(target, prop, receiver) {
return Reflect.get(target, prop, receiver);
},
set(_, prop, value) {
store.setCell(tableId, rowId, prop as CellIds[T], value);
return true;
}
};
}
export function useTable<T extends TableId>(store: Store, tableId: T) {
let table = $state(store.getTable(tableId));
let initialStore = true;
$effect(() => {
if (!initialStore) {
table = store.getTable(tableId);
}
initialStore = false;
const listener = store.addTableListener(tableId, () => {
table = store.getTable(tableId);
});
return () => {
store.delListener(listener);
};
});
return {
get value() {
return new Proxy(table, createTableHandler(store, tableId));
},
set value(value) {
store.setTable(tableId, value);
}
};
}
export function useRow<T extends TableId>(store: Store, tableId: T, rowId: Id) {
let row = $state(store.getRow(tableId, rowId));
let initialStore = true;
$effect(() => {
if (!initialStore) {
row = store.getRow(tableId, rowId);
}
initialStore = false;
const listener = store.addRowListener(tableId, rowId, () => {
row = store.getRow(tableId, rowId);
});
return () => {
store.delListener(listener);
};
});
return {
get value() {
return new Proxy(row, createRowHandler(store, tableId, rowId));
},
set value(newRow) {
store.setRow(tableId, rowId, newRow);
}
};
}
export function useCell<T extends TableId>(
store: Store,
tableId: T,
rowId: Id,
cellId: CellIds[T]
) {
let cell = $state(store.getCell(tableId, rowId, cellId));
let initialStore = true;
$effect(() => {
if (!initialStore) {
cell = store.getCell(tableId, rowId, cellId);
}
initialStore = false;
// weird type error, but it should still work
//@ts-ignore
const listener = store.addCellListener(tableId, rowId, cellId, () => {
cell = store.getCell(tableId, rowId, cellId);
});
return () => {
store.delListener(listener);
};
});
return {
get value() {
return cell;
},
set value(value) {
if (value === undefined) {
store.delCell(tableId, rowId, cellId);
} else {
store.setCell(tableId, rowId, cellId, value);
}
}
};
}
export function useValues(store: Store) {
let values = $state(store.getValues());
let initialStore = true;
$effect(() => {
if (!initialStore) {
values = store.getValues();
}
initialStore = false;
const listener = store.addValuesListener(() => {
values = store.getValues();
});
return () => {
store.delListener(listener);
};
});
return {
get value() {
return values;
},
set value(newValues) {
store.setValues(newValues);
}
};
} How this works is pretty cool. I'm using proxies to support updating nested attributes. And everything is nicely typed too :D <script lang="ts">
import manager from '$lib/store.svelte';
import { onMount } from 'svelte';
import Test from './Test.svelte';
let loaded = $state(false);
onMount(() => {
manager.init().then(() => {
loaded = true;
});
return () => {
// maybe close ws connection if you use a synchronizer
};
});
</script>
{#if loaded && manager.store}
<Test store={manager.store} />
{/if} Test.svelte <script lang="ts">
import { useCell, useRow, useTable } from '$lib/hooks.svelte';
import type { Store } from '$lib/store.svelte';
const {
store
}: {
store: Store;
} = $props();
let pets = useTable(store, 'pets');
let owners = useTable(store, 'owners');
let ownerRow = useRow(store, 'owners', 'owner');
let ownerLastName = useCell(store, 'owners', 'owner', 'lastName');
</script>
<!-- directly mutate cell -->
<input bind:value={ownerLastName.value} />
<!-- mutate from row -->
<input bind:value={ownerRow.value.lastName} />
<!-- or mutate from the table -->
<input bind:value={owners.value.owner.lastName} />
<pre>{JSON.stringify(
{
pets: pets.value,
owners: owners.value,
ownerRow: ownerRow.value,
ownerLastName: ownerLastName.value
},
null,
2
)}</pre> |
Beta Was this translation helpful? Give feedback.
-
What about Svelte?
Is there a way to use it with Svelte right now?
Beta Was this translation helpful? Give feedback.
All reactions