TaskFlow, by the end of the previous lesson, is already a complete and functional application: five assembled components, a shared context, a reactive controller, a mixin, and a simulated service, all coordinated exactly as planned in the first lesson of this module. Before the project can truly be called finished, what remains is to check with automated tests the pieces completed in this module that module 9 couldn't cover because they didn't exist yet, to bundle the application for deployment, and to decide whether any of its pieces deserve to be published separately as a reusable library. This is the last lesson of the course: it introduces no new Lit concepts, and closes, in its final section, the complete journey started in the lesson "What are Web Components and Why Lit?".
Contents
- What's left to close before finishing
- Final testing checklist: what 09-01 and 09-02 didn't get to cover
- New tests:
<task-filter>and the complete flow of an event - Final build with Vite: commands and the structure of
dist/ - Publishing
<task-card>as a reusable npm library - Deployment checklist: static site vs. library
- TaskFlow, from start to finish: a module-by-module review
- Next steps, beyond this course
- Closing the course
- What's left to close before finishing
Module 9 built TaskFlow's test suite (09-01), its accessibility (09-02), its performance (09-03), and its catalog of patterns and anti-patterns (09-04) on top of the application as it existed then: without a well-established <task-filter>, with <task-board> still on an earlier version of its initial load. This module has completed those pieces, and this lesson closes the circle: it extends the test suite to what was missing, gets the application ready to be published, and with that brings the entire course to a close.
- Final testing checklist: what 09-01 and 09-02 didn't get to cover
Before writing any new test, it's worth taking an honest inventory of which part of TaskFlow already has coverage and which doesn't, using exactly the same tools from the lesson "Unit Testing with Web Test Runner" (09-01):
| Component or flow | Has a test since module 9? | What's left to cover in this lesson |
|---|---|---|
<task-card>: title, status badge, expanding click |
Yes (09-01) | Nothing |
<task-card>: role, aria-expanded |
Yes (09-02) | Nothing |
<task-filter>: aria-label, aria-pressed, state change |
No | A test that checks that clicking a button updates the filter context |
<task-list>: filtering based on context |
No | An integration test that checks that the filter reduces the visible cards |
tarea-cambiada: the complete event, from <task-card> to <task-board> |
No | An integration test that checks the event-up / property-down cycle across two hops |
The first two rows were already resolved; the last three are exactly the pieces this module has added or completed, and they're the ones covered in the next section.
- New tests:
<task-filter> and the complete flow of an event
<task-filter> and the complete flow of an eventThe first new test checks <task-filter> in isolation, but to truly exercise its behavior it needs to be wrapped in a test context provider, since <task-filter> on its own, without any ancestor publishing filtroContext, would only see the fallback value defined in its own valorActual:
// test/task-filter.test.js
import { fixture, html, expect } from '@open-wc/testing';
import { ContextProvider } from '@lit/context';
import { LitElement } from 'lit';
import { filtroContext } from '../src/context/filtro-context.js';
import '../src/components/task-filter.js';
// A small test component that only publishes the context,
// exactly the same role that <task-board> plays in the real application.
class ProveedorDePrueba extends LitElement {
constructor() {
super();
this.valor = { texto: '', estado: 'todas', actualizar: (cambios) => this._actualizar(cambios) };
this._provider = new ContextProvider(this, { context: filtroContext, initialValue: this.valor });
}
_actualizar(cambios) {
this._provider.value = { ...this._provider.value, ...cambios };
this.valor = this._provider.value;
}
render() {
return html`<slot></slot>`;
}
}
customElements.define('proveedor-de-prueba', ProveedorDePrueba);
describe('task-filter', () => {
it('expone aria-pressed="true" solo en el botón del estado activo', async () => {
const contenedor = await fixture(
html`<proveedor-de-prueba><task-filter></task-filter></proveedor-de-prueba>`
);
const filtro = contenedor.querySelector('task-filter');
const botones = filtro.shadowRoot.querySelectorAll('button');
expect(botones[0].getAttribute('aria-pressed')).to.equal('true'); // "todas", active by default
expect(botones[1].getAttribute('aria-pressed')).to.equal('false');
});
it('actualiza el contexto compartido al pulsar un botón de estado', async () => {
const contenedor = await fixture(
html`<proveedor-de-prueba><task-filter></task-filter></proveedor-de-prueba>`
);
const proveedor = contenedor;
const filtro = contenedor.querySelector('task-filter');
const botonPendientes = filtro.shadowRoot.querySelectorAll('button')[1];
botonPendientes.click();
await filtro.updateComplete;
expect(proveedor.valor.estado).to.equal('pendiente');
});
});The second test checks the complete flow from section 2, third row: that a state change triggered inside the shadow root of a <task-card>, several levels below, ends up actually modifying <task-board>'s tareas array, exactly the path described in the map from lesson 10-01.
// test/task-board.test.js
import { fixture, html, expect, aTimeout } from '@open-wc/testing';
import '../src/components/task-board.js';
describe('task-board: flujo completo de tarea-cambiada', () => {
it('propaga un cambio de estado desde una task-card hasta el array tareas', async () => {
const tablero = await fixture(html`<task-board></task-board>`);
// The initial load from tareas-service.js takes 1200 ms, simulated with
// setTimeout; the same wait happens here as would happen in the browser.
await aTimeout(1300);
await tablero.updateComplete;
const lista = tablero.shadowRoot.querySelector('task-list');
const primeraTarjeta = lista.shadowRoot.querySelector('task-card');
const selector = primeraTarjeta.shadowRoot.querySelector('select');
selector.value = 'hecha';
selector.dispatchEvent(new Event('change'));
await primeraTarjeta.updateComplete;
await lista.updateComplete;
await tablero.updateComplete;
expect(tablero.tareas[0].estado).to.equal('hecha');
});
});aTimeout, also imported from @open-wc/testing, is a small utility that returns a promise that resolves after the given number of milliseconds; it's used here because the simulated load from tareas-service.js (lesson 07-03, consolidated in 10-03) takes real time, not artificial, and the test needs to wait that same amount of time before any <task-card> exists to act on. The rest of the test chains three different updateComplete calls —for the card, the list, and the board— because, as explained in the lesson "Parent-to-Child Communication with Properties" (05-03) and in "Communication Patterns between Sibling Components" (05-04), the event crosses two forwardings before reaching <task-board>, and each level needs to complete its own update before the next one reflects it.
- Final build with Vite: commands and the structure of
dist/
dist/With the tests now complete, TaskFlow's production build as an application follows exactly the criteria set in the lesson "Bundling, Publishing, and TypeScript" (08-04, sections 1 and 4): Vite, not Rollup directly, because TaskFlow is a self-sufficient application, not a library meant for another bundler to reprocess.
The result, in dist/, looks like this:
dist/
├── index.html
└── assets/
├── index-a1b2c3d4.js
├── index-e5f6g7h8.css
└── task-board-i9j0k1l2.jsEach file name includes a hash fragment derived from its content (a1b2c3d4, in the example), exactly the cache-control mechanism explained in 08-04: if a file's content doesn't change between two successive deployments, its name doesn't change either, and the browser of someone who already visited TaskFlow can keep using the copy it already has cached without downloading it again. index.html, regenerated by Vite on every build, already points to the correct file names for that specific build, with no need to manually edit any path.
- Publishing
<task-card> as a reusable npm library
<task-card> as a reusable npm libraryTaskFlow, as a complete application, is deployed with the build from the previous section. But a different question fits here, already raised in the abstract in lesson 08-04: is it worth publishing <task-card> separately, so other projects —not just TaskFlow— can install it as an npm dependency? If the answer were yes (reasonable, given that <task-card> doesn't depend on any TaskFlow-specific data except what it receives as properties), the package.json of that standalone package would need to declare lit as an external dependency, not bundled in, for exactly the reason explained in 08-04 (section 3): avoiding two third-party components, both dependent on Lit, loading two full copies of the library in the same application.
{
"name": "@taskflow/task-card",
"version": "1.0.0",
"type": "module",
"main": "dist/task-card.js",
"module": "dist/task-card.js",
"files": ["dist"],
"peerDependencies": {
"lit": "^3.0.0"
},
"devDependencies": {
"lit": "^3.0.0",
"@rollup/plugin-node-resolve": "^15.0.0",
"rollup": "^4.0.0"
}
}peerDependencies is the piece that formalizes that decision: it declares that the package expects whoever installs it to provide their own copy of lit, rather than bundling its own. devDependencies keeps a copy of lit available only during the package's own development (to run its tests or its build), without that copy traveling inside the final published package. The Rollup configuration, identical to the one in lesson 08-04, produces the dist/task-card.js that this package.json references:
// rollup.config.js (of the @taskflow/task-card package, not of the TaskFlow application)
import resolve from '@rollup/plugin-node-resolve';
export default {
input: 'src/task-card.js',
external: ['lit', /^lit\//],
output: {
file: 'dist/task-card.js',
format: 'esm',
},
plugins: [resolve()],
};external: ['lit', /^lit\//] is the detail that was missing from the example in lesson 08-04: it tells Rollup not to include, in the final bundle, either the lit package itself or any of its submodules (lit/directives/..., lit/decorators.js), leaving those imports intact in the output so that the bundler of whoever installs @taskflow/task-card resolves them with their own copy of Lit, consistent with the peerDependencies declaration in the package.json.
- Deployment checklist: static site vs. library
| Step | As a complete application (dist/ from section 4) |
As a library (@taskflow/task-card from section 5) |
|---|---|---|
| Build tool | Vite (vite build) |
Rollup, with lit as an external dependency |
| Does it include a copy of Lit? | Yes, the bundle is self-sufficient | No, it's declared as a peerDependency |
| Where it's published | A static file server or a CDN | The npm registry |
| Who consumes it | People who visit TaskFlow directly in their browser | Other projects, as one more dependency in their own package.json |
| What tests must pass first | The whole test/*.test.js suite (module 9 and this lesson) |
The same tests, run against the package itself before publishing each version |
Both paths are legitimate and not mutually exclusive: nothing prevents TaskFlow from being deployed as a complete application (section 4) while, in parallel, <task-card> is also published as a standalone package (section 5) for other teams to reuse outside of TaskFlow, exactly the same component in both cases, just bundled in two different ways depending on who each result is aimed at.
- TaskFlow, from start to finish: a module-by-module review
With TaskFlow now tested, bundled, and ready to be deployed, it's worth looking back and covering, at a glance, the whole path of this course:
| Module | What it brought to TaskFlow |
|---|---|
| 1. Introduction to Lit and Web Components | The first Lit component, with no data or interaction yet. |
| 2. Reactive Templates and Rendering | The template engine, conditionals, lists, and the rendering cycle. |
| 3. Properties and Reactive State | titulo, estado, prioridad, expandida, and the fechaLimite converter. |
| 4. Styling Lit Components | Shadow DOM, shared styles, theming CSS variables, and <user-avatar> with slots. |
| 5. Events and Communication between Components | tarea-cambiada, the lifting-state-up pattern, and the birth of <task-board>. |
| 6. Lifecycle and Advanced Behavior | ContadorTiempoRestanteController and the ConEstadoCarga mixin. |
| 7. Directives and Advanced Template Features | classMap, resaltarSiUrgente, until, and <task-filter> with @lit/context. |
| 8. Integration, Interoperability, and Deployment | Use in plain HTML and other frameworks, SSR, bundling with Rollup and Vite. |
| 9. Testing and Best Practices | The first test suite, accessibility, performance, and the catalog of anti-patterns. |
| 10. Project: Building TaskFlow | Consolidated architecture, unified components, and this lesson's final close. |
No row in this table is a surprise at this point in the course; it's included here, in the last lesson, precisely so the whole journey can be seen in one go, something no earlier lesson, focused on its own module, could offer.
- Next steps, beyond this course
This course has covered Lit exhaustively for the kind of application TaskFlow represents, but the ecosystem around Lit keeps growing, and it's worth pointing out a few concrete paths for anyone who wants to continue beyond this final lesson:
@lit-labs/virtualizer: the lesson "Performance and Optimization" (09-03, section 9) mentioned virtualization as a technique for lists of tens of thousands of elements, beyond the practical scope of this course, and noted that TaskFlow didn't need it. It's, literally, the natural next step if<task-list>had to grow well beyond the data volumes handled in module 9.@lit-labs/motion: an experimental package from Lit's own ecosystem for declarative animations between states, an area this lesson has only touched manually with the CSS animation ofresaltarSiUrgente(lesson 10-02).@lit-labs/observers: reactive controllers already written by the Lit team to wrapResizeObserver,IntersectionObserver, and other browser observation APIs, following exactly the sameReactiveControllerpattern studied in lesson 06-03 withContadorTiempoRestanteController.- Exploring Lit with TypeScript and decorators in more depth: lesson 08-04 presented the syntactic equivalence between
static propertiesand decorators; progressively migrating TaskFlow, component by component, is a natural hands-on exercise to consolidate that lesson with a real project. - The official documentation at lit.dev: in particular its recipes section (Playground) and its release notes, to keep a close eye on any future API changes as Lit evolves beyond the version studied in this course.
- Closing the course
This course began, in the lesson "What are Web Components and Why Lit?", with a question about web platform standards and a promise: to learn how to build reactive interfaces with the browser's own components, without depending on a full application framework. Ten modules and thirty-nine lessons later, that promise has been fulfilled with a real, complete application, TaskFlow, built piece by piece, module by module, without leaving any loose ends: every concept presented was immediately applied to the project itself, and every piece mentioned without being fully resolved —from <task-filter> in module 5 to the resaltarSiUrgente animation in this final lesson— found its place before the course ended.
Anyone who has followed the whole course, writing and testing every example, already has the judgment needed to tackle a real project with Lit: when to lift state to a common ancestor and when to reach for a shared context; when a reactive controller and when a mixin; when until and when an explicit loading property; and, above all, the habit of asking, faced with every design decision, which of the two alternatives better avoids the anti-patterns cataloged in module 9. That judgment, more than memorizing a specific API, is what survives once a course is over.
Congratulations on making it this far. TaskFlow is finished, tested, and ready to be deployed; the rest of the road, with Lit or with any other tool, is now in your hands.
Common Mistakes and Tips
- Publishing a library without declaring
litas an external dependency: as explained in section 5, and already warned about in lesson 08-04, forgettingexternalin the Rollup configuration would include a full copy of Lit inside the published package, with the already-known duplication risk. - Running
vite buildexpecting the same result as for a library: as the table in section 6 reminds us, each deployment path has a different tool and configuration; mixing both goals into the same build configuration usually produces a result that doesn't serve either case well. - Accepting an incomplete test suite just because module 9 already started it: as seen in section 2,
<task-filter>and the completetarea-cambiadaflow had no tests until this lesson; before considering any project fully tested, it's worth reviewing which pieces were added after the last time tests were written. - Treating this lesson as the end of learning, not just of the course: as section 8 points out, the Lit ecosystem stays alive beyond these forty lessons; the judgment learned here is the foundation for continuing to explore, not a final stop.
Exercises
- Add a test to
test/task-board.test.jsthat checks that, after changing the search text in<task-filter>'s<input>(accessible fromtablero.shadowRoot.querySelector('task-filter').shadowRoot.querySelector('input')), the number of visible<task-card>elements inside<task-list>decreases accordingly. Remember to wait forupdateCompleteat each level involved, as in the test in section 3. - Complete the
package.jsonfrom section 5 by adding a"build": "rollup -c"script and explain, based on lesson 08-04 (section 2), why this package uses Rollup while the complete TaskFlow application, in section 4 of this lesson, uses Vite for the same verb "to build". - Review the table in section 7 and, for the module you think brought the most important change to TaskFlow's overall architecture (not to a specific technique, but to how the components are organized among themselves), justify your choice with a concrete example from the project itself.
Solutions
it('reduce las tarjetas visibles al escribir en el filtro de texto', async () => {
const tablero = await fixture(html`<task-board></task-board>`);
await aTimeout(1300);
await tablero.updateComplete;
const filtro = tablero.shadowRoot.querySelector('task-filter');
const lista = tablero.shadowRoot.querySelector('task-list');
const input = filtro.shadowRoot.querySelector('input');
const tarjetasAntes = lista.shadowRoot.querySelectorAll('task-card').length;
input.value = 'demo';
input.dispatchEvent(new Event('input'));
await filtro.updateComplete;
await lista.updateComplete;
const tarjetasDespues = lista.shadowRoot.querySelectorAll('task-card').length;
expect(tarjetasDespues).to.be.lessThan(tarjetasAntes);
});- This package uses Rollup because its goal, as explained in lesson 08-04 (section 1) and recalled in section 6 of this lesson, is to produce a library reusable by other bundlers, not a self-sufficient application ready to be served directly; Rollup produces a clean output close to the source code, designed precisely so an external bundler (that of whoever installs
@taskflow/task-card) can reprocess it according to its own needs. The complete TaskFlow application, on the other hand, has no external consumer that will bundle it again: its goal is the final result already optimized for the browser, with chunk splitting and hash-based cache management, which Vite provides out of the box (using Rollup underneath, but with a configuration aimed at applications, not libraries). - A reasonable answer points to module 5, with the appearance of
<task-board>: before that lesson,<task-list>kept its own array of sample tasks and no component existed capable of coordinating more than two levels of the hierarchy; starting with "Communication Patterns between Sibling Components" (05-04), TaskFlow's whole architecture came to be organized around a common ancestor that distributes properties downward and gathers events upward, the same pattern that, extended with the filter context in module 7, remains in force with no structural changes up to the final version consolidated in this module 10. Other equally valid answers could point to module 7, for introducing@lit/contextas an alternative to manually forwarding properties, or module 4, for the appearance of<user-avatar>as the first example of decomposing into a reusable child component.
Lit Course
Module 1: Introduction to Lit and Web Components
- What are Web Components and why Lit?
- Setting Up the Development Environment
- Your First Lit Component
- Anatomy of a Lit Component
Module 2: Reactive Templates and Rendering
- Lit's Template Engine
- Expressions and Interpolation in Templates
- Conditional Rendering
- List Rendering
- The Rendering Cycle
Module 3: Reactive Properties and State
- Reactive Properties
- Internal State with @state
- Types of Properties and Custom Converters
- Attributes vs Properties and Reflection
Module 4: Styling Lit Components
- Encapsulated CSS with Shadow DOM
- Shared Styles Between Components
- Custom CSS Properties and Theming
- Slots and Styling Distributed Content
Module 5: Events and Component Communication
- Handling DOM Events in Templates
- Custom Events: Communication from Child to Parent
- Communication from Parent to Child with Properties
- Communication Patterns Between Sibling Components
Module 6: Lifecycle and Advanced Behavior
- Lifecycle Callbacks
- Reactive Hooks: willUpdate, updated, and firstUpdated
- Reactive Controllers
- Mixins and Composing Behavior
Module 7: Directives and Advanced Template Features
- Built-in Directives: classMap, styleMap and ifDefined
- Custom Directives
- Asynchronous Rendering with until
- Shared Context with @lit/context
Module 8: Integration, Interoperability and Deployment
- Using Lit Components in Plain HTML
- Integrating Lit with React, Vue, and Angular
- Server-Side Rendering with @lit-labs/ssr
- Bundling, Publishing, and TypeScript
Module 9: Testing and Best Practices
- Unit Tests with Web Test Runner
- Accessibility in Web Components
- Performance and Optimization
- Common Patterns and Anti-patterns
