@educspace/eval-element
v0.4.0
Published
Redistributable <exercise-element> web component: renders and grades educspace exercises on any web page.
Readme
@educspace/eval-element
<exercise-element> — the educspace exercise widget as a single script tag,
usable on any web page.
<script type="module" src="/assets/eval-element.js"></script>
<div class="stv-exercise" data-exercise-id="…">
<div class="stv-exercise-prompt">
<p>Quel est le type de <code>3.14</code> ?</p>
</div>
<script type="application/json" class="stv-exercise-data">
{ … Question JSON … }
</script>
</div>Loading the script registers the element and upgrades every mount on the page.
Nothing else is needed: no framework on the host page, no stylesheet to include,
no configuration. demo/index.html is a complete working page.
The mount markup is what stevenson's exercise filter emits, but nothing here is stevenson-specific — any generator (or hand-written HTML) can produce it.
What it does
Renders one exercise of any supported kind (free text, single/multiple choice,
ordered list, code, web, sql), runs it in the browser (Pyodide for Python,
sql.js for SQL, a sandboxed iframe for web) and grades it client-side through
@educspace/eval-engine.
Every verdict is also dispatched as an eds-graded DOM event (bubbling and
composed), so a host page can observe results:
document.addEventListener('eds-graded', (e) => {
const { correct, payload, meta, questionId } = e.detail;
});Tracking (optional)
By default the element is anonymous and client-side, and makes no network calls. A page opts into tracking with two meta tags:
<meta name="questions-server" content="https://console.questions.educ.space" />
<meta name="questions-teacher" content="jmb" />
<!-- optional: where the student sees their sheets and results -->
<meta name="questions-me" content="https://me.questions.educ.space" />With those present, a tracked exercise (see the mount contract below) shows a
discreet strip: an invitation to log in, then the student's badge — solved or
not, how many recorded attempts, and whether the statement has changed since.
Every Check is declared to the server with the answer, the client's verdict, the
version answered, and the time spent.
What it does not do is get in the way:
- not logged in, no server configured, server unreachable → the exercise works exactly as it does untracked;
- an attempt that cannot be sent is queued in
localStorageand replayed on the next load; - an attempt refused by a policy (attempt cap, cooldown) still grades and shows its verdict — only the recording is skipped, with the server's explanation.
One login per browser per site: the token is shared by every exercise on every page of that site, and the whole page's badges are fetched in a single request.
With questions-me set, a logged-in student also gets a link to their own
page of sheets and results, carrying the teacher slug so they never have to know
it.
Mount contract
| | |
| -------------------------- | ------------------------------------------------------------------ |
| div.stv-exercise | the mount, replaced by an <exercise-element> |
| .stv-exercise-prompt | prompt HTML, pre-rendered by the generator (Markdown, MathML) |
| script.stv-exercise-data | the Question as camelCase JSON |
| data-exercise-id | question UUID |
| data-exercise-ref | author-chosen id — the identity a tracking server knows |
| data-exercise-mode | tracked / assessed / pool; absent means anonymous |
| data-exercise-version | hash of the exercise source, recorded with an attempt |
| data-exercise-pool | the pool: which one this exercise belongs to, or a slot draws from |
| data-exercise-slot | on a pool slot, the identity its draw is remembered against |
| data-exercise-title | heading for a mount that has no statement to provide one |
Only the mount class and the payload are required; everything else is tracking metadata, and an exercise without it is anonymous: client-side, nothing recorded.
A mount in assessed or pool mode carries no payload: the statement lives
on the server and is fetched once the student has logged in (an assessed
exercise by id, a pool slot by drawing a member). Until then the box says why it
is empty rather than pretending to be an exercise. A served statement comes as
Markdown, rendered by the element; when the site that published it also sent
the statement rendered through its own pipeline (promptHtml), that HTML is
used instead — sanitised — so callouts, includes and the like look exactly as
they do in the surrounding page. Assessed answers are withheld
until the deadline passes, at which point the same exercise is served whole and
becomes its own correction, and it is the server's verdict — never the
browser's — that the student is shown.
Theming
The element renders into a shadow root: the host page's CSS cannot reach in and its own cannot leak out. Theming therefore goes through CSS custom properties, which do cross the boundary:
exercise-element {
--eds-exo-accent: #7c3aed;
--eds-exo-surface: #faf5ff;
--eds-exo-radius: 14px;
}The full list is documented at the top of src/exercise/styles.ts.
Coexisting with other bundles
The element ships its own Angular runtime, so a page may well end up running two Angular applications (the site's own template and this one). That is fine under three conditions, all of which this package satisfies:
- Zoneless. zone.js patches globals once; two copies fight. This bundle
uses
provideZonelessChangeDetection()and so must any other bundle on the page. - One owner per custom-element name. The registry is page-global and a name
defines once. This bundle owns
exercise-elementand guards itsdefine(), so loading it twice is harmless — but no other bundle may define that name. - Style ownership. Shadow DOM, per above. Tailwind (or any utility CSS from the host page) does not cross into the element, and must not be relied on.
Build
npm install
npm run build # dist/eval-element.jsThe build refuses to emit more than one script: the element must stay a single
file, so avoid dynamic import() in src/. Monaco is the deliberate exception
— it is fetched from a CDN on demand, only when a code/sql/web exercise is
actually present, and its stylesheets are copied into the shadow root
(src/exercise/monaco-loader.ts).
