Open kaljarv opened 1 month ago
It seems like we can’t use only stores when using SSR (docs, this discussion). In the candidate app all stores are contained in one context that is set on the root layout of the candidate app. So maybe one solution would be to make something similar in the voter app.
With very temporary user data in addition to variables or stores, maybe snapshots (https://kit.svelte.dev/docs/snapshots) could be used, at least in some situations.
global
(maybe even separate translations that are needed by all components into components
with global
having data used by both the Voter and the Candidate App)vaa-data
: elections, questions, candidates etc.voter
: voterAnswers
, temporarily stored user choices, matching resultscandidate
: unpublished and multi-lingual candidate answers, auth status, write api functions$lib/components
: simple components only having access to the components
ctx with most of their props provided the standard way, although these might be vaa-data
objects, such as Candidate
which proffers access to Party
etc.$lib/voter/components
: complex components having direct access to the voter
and global
ctxs, e.g. <EntityFilters>
$lib/candidate/components
: complex components having access to the candidate
and global
ctxs<EntityDetails>
, for example, which needs access to questions
? Perhaps the divide should be between components needing access to just translations and those to also vaa-data
…
interface VoterContext {
candidates: Writable<CandidateProps[] | undefined>;
candidateData: Promise<CandidateDatum[]>;
}
candidateData.then(d => candidates.set(convertPOJOtoProps(d)));
// In Svelte 5, this we'll possibly refactor this to
candidateData: Promise<CandidateDatum[]> | undefined = undefined;
candidates = $derived.by(async () => {
if (!candidateData) return Promise.resolve(undefined);
return candidateData.then(d => convertPOJOtoProps(d));
});
// In +layout.server.ts
load() {
return {candidateData: DataProvider.getCandidates()};
}
// In +layout.ts / +layout.svelte
export let data; // Or await parent if +layout.ts
ctx.candidateData = data.candidateData;
// In .svelte
{#if ctx.candidates}
Render
{:else}
Loading
{/if}
import { enhance, applyAction } from '$app/forms';
<form action="/api/feedback" use:enhance={() => async ({results}) => await applyAction(results)}> // ...
A quick list of the main functionalities of the app so those can also be divided into contexts or such.
DataProvider
vaa-data
vaa-data
model constituencyId
etc. route params in the urls if they are not neededDataProvider
or the default local functionDataProvider
or DataWriter
DataProvider
or the default local functionlocalStorage
DataProvider
or DataWriter
localStorage
DataProvider
server-dependenceScenarios:
DataProvider
functions run on the client
DataProvider
functions may run both on the client and the server
DataProvider
functions run only on the server
Implementation:
StrapiDataProvider
is available on CSR, perhaps directly from vaa-data
LocalDataProvider
is available via API routesLocalDataProvider
and StrapiDataProvider
are available via API routesDataProvider
directly via vaa-data
LocalDataProvider
and StrapiDataProvider
are available via API routes or as server-only modulesIn final consumers: components, pages, derived stores
// EntityDetails.svelte
<script type="ts">
export let content: MaybeRanked<Entity>;
const { entity, match } = parseWrapped(content);
</script>
<h1>{ entity.name }</h1>
{#if match}
<Match {match}/>
{/if}
<EntityOpinions {entity}/>
// EntityOpinions.svelte
<script type="ts">
import { getContext } from 'svelte';
export let entity: Entity;
const { questionCategories } = getContext<VAADataContext>('vaa-data').data;
const voter = getContext<VoterContext>('voter').voter;
</script>
// There should be filters for showing questions, e.g. related to only a specific election
{#each $questionCategories as category}
<h2><CategoryTag {category}/></h2>
{#each category.questions as question}
<Question
variant="variant"
{question}
{entity}
referenceEntity={$voter}/>
{/each}
{/each}
// Question.svelte
<script type="ts">
export let variant: QuestionVariant = 'voterAnswer';
export let question: Question;
export let entity: Entity | Voter; // Voter may be also a subtype of Entity
export let referenceEntity: Entity | Voter;
</script>
<h1>{ question.text }</h1>
<AnsweringButtons
mode={variant === 'voterAnswer' ? 'answer' : 'display'}
selected="entity.getAnswer(question)"
reference="referenceEntity.getAnswer(question)"/>
// $voter/context.ts
export interface VoterContext {
/** A reference to the data root from VAADataContext, supplied when the context is created. */
vaaData: DataRoot;
matchingResults: ?;
voter: VoterStore;
}
export interface VoterStore {
getAnswer: (question: Question) => AnswerValue;
}
Common to both types:
// +layout.svelte OR derived store
import { getContext } from 'svelte';
const ctx = getContext<GlobalContext>('global');
export let data: LayoutData;
const { candidatesData } = data;
ctx.data.provideCandidates(candidatesData);
// +layout.ts
import { dataProvider } from '$lib/api';
export async function load({fetch}) {
const dp = await dataProvider;
dp.fetch = fetch;
return { candidatesData: dp.getCandidates() };
}
Server-independent:
// $lib/api/dataProvider/strapi/strapiDataProvider.ts
export async function getCandidates(options?): Promise<CandidateDatum[]> {
return dataProvider.fetch(
PUBLIC_API + '/api/candidates'
).then(d => convertStrapiToVAAData(d));
}
Server-dependent:
// Generic interface for server-dependent data providers
// $lib/server/api/dataProvider/serverDependentDataProvider.ts
export async function getCandidates(options?): Promise<CandidateDatum[]> {
return dataProvider.fetch(
'/api/data/candidates'
);
}
// Generic api route for server-dependent data providers
// /routes/api/data/+server.ts
import { json } from '@sveltejs/kit';
import type { RequestHandler } from './$types';
import { serverDataProvider } from '$server/api';
export const GET: RequestHandler = async ({ url }) => {
const collection = url.searchParams.get('collection');
if (collection === 'candidates') {
const data = await serverDataProvider.then({getCandidates} => getCandidates());
}
return new json(data);
}
// Data provider implementation
// $lib/server/api/dataProvider/local/localServerDataProvider.ts
import { read } from '$app/server';
export async function getCandidates(options?): Promise<CandidateDatum[]> {
return read(DATA_FOLDER + 'candidates.json')
.json().then(d => convertLocalPOJOToVAAData(d));
}
Hard to do if on /results/canditate/5
:
results
layout loads all candidates and 5
just picks one from them.
Possible option:
/results/canditate/5
or even /constituency/1/results/canditate/5
and this uses a layout to load all entities./link/canditate/5
or similar, which only loads the necessary candidate. This way we also bypass problems with including constituency and other details in the url.Selections:
Options:
/[electionId]/[constituencyId]/questions
/[electionId]/[constituencyId]/results/[entityType]/[entityId]
StorageAPI
/results/[entityType]/[entityId]
Also, consider whether to prefer stores over contexts, e.g. in temporary preferences regarding questions and categories. (I'm doing this atm.)
Things to consider
page
store cannot be subscribed to outside of components)Requirements
vaa-data
) on CSR, likely stuffed in stores [^1]candidateRankings
fromcandidates
,voterAnswers
andsettings
undefined
,[]
, andCandidateProps[]
localStorage
)sessionStorage
)[^1] This is currently implemented in a possibly anti-pattern-y way such that the
+layout/page.server.ts: load
functions load data from thegetData
api and return it as part of the page data. The stores on CSR then access this data by subscribing to thepage
store, which holds all data thus loaded inpage.data
, and sometimes process this data a bit.Another possibility might be to still load the data with the
+layout.server.ts
files but then always accompany them with a universal+layout.ts
file to process the data – or use only the latter (which would require creating making the data API universally available but it's most likely done anyway). See Sveltekit docs.