Книга: Build a Frontend Web Framework (From Scratch)
Назад: 11.2.4 Emitting events
На главную: Предисловие

render() {

const { isLoading, todos } = this.state

if (isLoading) {

return h('p', {}, ['Loading...'])

}

return h('ul', {}, todos.map((todo) => h('li', {}, [todo])))

}

})

The first thing you’d naturally do is mock the fetch() function to return a promise that resolves with the data you want to test:

import { vi } from 'vitest'

// Mock the fetch API

global.fetch = vi.fn().mockResolvedValue({

json: vi.fn().mockResolvedValue([

'Feed the cat',

'Mow the lawn',

]),

})

That code had a little trick in it: fetch() returns a promise that resolves with a response object that has a json() method that returns a promise that resolves with the data.

That sentence is a mouthful, but the concept isn’t complicated. In any case, you’ve

14.1

Testing components with asynchronous behavior: nextTick()

323

already mocked the call to the server. To test that the loading indicator is rendered while the data is being fetched, you can do the following:

test('Shows the loading indicator while the todos are fetched', () => {

// If nextTick() isn't awaited for, the onMounted() hooks haven't run,

// so the loading should be displayed.

expect(document.body.innerHTML).toContain('Loading...')

})

It’s that easy! If you don’t await for the nextTick() function, the onMounted() hook hasn’t run yet, so the loading indicator is displayed. But at some point after the test has finished, the DOM will be updated and the loading indicator will disappear. You want to clean up the DOM after each test so that the next test starts with a clean DOM.

The only thing you need to do is await for the nextTick() function before unmounting the application, as shown in bold in the following snippet:

afterEach(async () => {

await nextTick()

Waits for the nextTick() function

app.unmount()

before unmounting the application

})

Note that if you had onUnmounted() asynchronous hooks, you’d also want to await for the nextTick() function after unmounting the application:

afterEach(async () => {

await nextTick()

app.unmount()

await nextTick()

})

To test that the list of to-dos is rendered after the data is fetched, you can do the following:

test('Shows the list of todos after the data is fetched', async () => {

// The nextTick() function is awaited for, so the onMounted() hooks

// have finished running, and the list of todos is displayed.

await nextTick()

expect(document.body.innerHTML).toContain('Feed the cat')

expect(document.body.innerHTML).toContain('Mow the lawn')

})

Waits for the nextTick() for the data to be

fetched and the component to re-render

A much better approach would be to add data-qa attributes to the elements in the DOM so that you can select them easily and then check their text content. But this example is simplified to show how to test components with asynchronous hooks.

324

CHAPTER 14

Testing asynchronous components

The same nextTick() function could test components that have asynchronous event handlers, such as a button that loads more data from a server when clicked, so you may wonder how you implement the nextTick() function. I’m glad you asked!

Let’s implement it. Make sure that you have a clear understanding of event loops, tasks, and microtask queues, because that knowledge is key to understanding what comes next. Buckle up!

14.1.2 The fundamentals behind the nextTick() function

The nextTick() function might appear to be magical, but it’s based on a solid understanding of the event loop’s mechanics. Some of the big frameworks implement similar functions, which are widely used for testing components with asynchronous behavior, so I’m positive that implementing your own function will help you understand how it works and when to use it.

The nextTick() function in some libraries

The nextTick() function defined in Vue can be used both in production code and in tests. Its usefulness resides in the fact that the code that you run after awaiting for the nextTick() function is executed after the component has re-rendered, when you know that the DOM has been updated. This function is documented at

. You can also check how it’s implemented in the runtime-core package, inside the scheduler.ts file,

Node JS implements a process.nextTick() function that’s used to schedule a callback to be executed on the next iteration of the event loop. You can find the documentation for it at I recommend that you give this documentation a read because it’s short and well explained.

Svelte defines a tick() function that returns a Promise that resolves when the component has re-rendered. Its use is similar to that of the nextTick() function in Vue.

This function is documented at You can check its implementation in the runtime.js file inside the internal/client/ folder at

The nextTick() function returns a Promise that resolves when all the pending jobs in the scheduler have finished executing. So the question is how you know when all the pending jobs in the scheduler have finished executing. First, you want to call the scheduleUpdate() function to make sure that when the nextTick() function is called, the processJobs() function is scheduled to run:

export function nextTick() {

scheduleUpdate()

// ...

}

If you remember from chapter 13, when the execution stack empties, the event loop executes all the microtasks first; then it executes the oldest task in the task queue, lets

14.1

Testing components with asynchronous behavior: nextTick()

325

the DOM render, and starts over. You’ve queued the scheduleUpdate() function to run as a microtask, so you know that as soon as the execution stack is empty, the processJobs() function will be executed (figure 14.1).

When the execution stack empties, the

processJobs() function starts executing.

Microtasks

processJobs()

Ta

T sks

DOM

T1

T2

Execution stack

Event loop

Figure 14.1

The processJobs() function is scheduled to run as a microtask.

All the jobs in the scheduler are asynchronous because even if they weren’t originally, you’ve wrapped them inside a Promise and set up a then() callback to handle the result or rejection of the promise. As a result, the processJobs() function will queue more microtasks as it runs jobs (figure 14.2).

The processJobs() function schedules the

microtasks created by the jobs it executes.

Microtasks

job2()

μT1

μT2

μT3

job1()

Tasks

processJobs()

DOM

T1

T2

Execution stack

Event loop

Figure 14.2

The processJobs() function queues more microtasks.

The event loop won’t take the next task until all microtasks have been executed, including the microtasks that are queued by the processJobs() function. So to make sure that you resolve only the Promise returned by the nextTick() function when all the pending jobs in the scheduler finish executing, you need to schedule a task whose callback resolves the Promise returned by the nextTick() function. You can use setTimeout() to schedule a new task:

new Promise((resolve) => setTimeout(resolve))

326

CHAPTER 14

Testing asynchronous components

By scheduling the created Promise in the task callback, you’re forcing the event loop to go through at least another cycle before resolving the Promise because the resolve() function inside a Promise is scheduled as a microtask. So if the microtask is queued only when a task is executed, the event loop needs to process all pending microtasks, process the task that resolves the Promise, and then process all the pending microtasks until that resolve() is executed. Only then does awaiting for the Promise returned by the nextTick() function resolve (figure 14.3).

Other microtasks might be scheduled

This new microtask finishes after

before the resolve() task is executed.

every other scheduled microtask.

Microtasks

μT1

μT2

μT3

...

then()

Tasks

T1

T2

resolve()

Execution stack

Event loop

When this task is executed, it

enqueues a new microtask.

Figure 14.3

The Promise returned by the nextTick() function resolves when all the pending jobs in the scheduler have finished executing.

This process of scheduling a microtask via a task is commonly referred to as flushing promises. Many frontend and testing frameworks include a function to flush pending promises, mostly for testing purposes, so that you can easily write your unit tests without worrying about the asynchronous behavior of the components.

I know that this concept might be hard to wrap your head around, but you must understand how the event loop works to understand how the nextTick() function works. Take your time processing this information. If you need to, read this chapter again, draw the diagrams yourself, and experiment with the browser’s console. In section 14.1.3, I’ll ask you to do just that.

14.1.3 Implementing the nextTick() function

You should understand how the nextTick() function works by now, so you’re ready to implement it. Inside the scheduler.js file, add the code in listing 14.1.

Listing 14.1

nextTick() and flushPromises() (scheduler.js)

export function nextTick() {

scheduleUpdate()

return flushPromises()

}

14.1

Testing components with asynchronous behavior: nextTick()

327

function flushPromises() {

return new Promise((resolve) => setTimeout(resolve))

}

Before publishing the framework’s next version—version 4.1, including the nextTick() function—I want you to complete exercise 14.1. It challenges you to guess what the order of some console.log() calls will be—a classic JavaScript interview question—and will help you understand the principle behind the flushPromises() function.

Exercise 14.1

Open the browser’s console, and write this slightly modified version of the flushPromises() function:

function flushPromises() {

return new Promise((resolve) => {

console.log('setTimeout() with resolve() as callback...')

setTimeout(() => {

console.log('About to resolve the promise...')

resolve(42)

})

})

}

Note the two console logs:

 One inside the Promise body, before the setTimeout() function is called

 One inside the setTimeout() callback, right before the resolve() function is called

Now write this doWork() function, which schedules both microtasks and tasks and then awaits for the Promise returned by the flushPromises() function: async function doWork() {

queueMicrotask(() => console.log('Microtask 1'))

setTimeout(() => console.log('Task 1'))

queueMicrotask(() => console.log('Microtask 2'))

setTimeout(() => console.log('Task 2'))

const p = flushPromises()

queueMicrotask(() => {

console.log('Microtask 44')

queueMicrotask(() => console.log('Microtask 45'))

})

setTimeout(() => console.log('Task 22'))

const value = await p

console.log('Result of flush:', value)

}

328

CHAPTER 14

Testing asynchronous components

(continued)

Can you guess the order in which all these console.log() calls will be executed when you execute the doWork() function? Draw a diagram with the task and microtask queues to help you figure it out. Then run the doWork() function and compare the result with your expectations.

Find the solution .

14.2

Publishing version 4.1 of the framework

Let’s publish the new version of the framework. First, you want to include the nextTick() function as part of the public API of the framework. Including it allows developers to use this function inside their unit tests, as you saw in section 14.1. In the index.js file, add the import statement shown in bold:

export { createApp } from './app.js'

export { defineComponent } from './component.js'

export { DOM_TYPES, h, hFragment, hString } from './h.js'

export { nextTick } from './scheduler.js'

Then, in the package.json file, update the version number to 4.1.0:

"version": "4.0.0",

"version": "4.1.0",

Remember to move your terminal to the root or the project. Then run npm install so that the package version number in the package-lock.json file is updated: $ npm install

Finally, place your terminal inside the packages/runtime folder, and run $ npm publish

The new version of your framework, which includes the nextTick() function, is published.

14.3

Where to go from here

Congratulations on reaching the final chapter of the book! By now, you’ve achieved an impressive learning milestone. Your grasp of how frontend frameworks work sur-passes that of 99 percent of developers in this field. Even more remarkable, you’ve not only delved into the theory, but also mastered the practical aspect of frontends by building your own framework.

You may be eager to continue enhancing your skills. Consider adding a router to enable seamless transitions among pages and incorporating a template compiler into your build process. Perhaps you’re even contemplating creating a Chrome/Firefox

14.3

Where to go from here

329

extension to debug applications written with your framework. These features are common in frontend frameworks, and understanding how to implement them can further elevate your expertise. Originally, I intended to cover these topics in the book, but due to the book’s length constraints, they had to be omitted.

Nevertheless, I wanted to cover this material, so I had two options: write an extra part for this book or publish the material online. The second option seemed to be a better idea, so I’ve published some extra chapters in the wiki section of the book’s code repository ). I suggest that you continue your learning journey by reading the chapters in the wiki; they follow a similar structure to the chapters in this book, so you should feel right at home. The material covered in the wiki is more advanced, however, so be ready to embrace the challenge.

When you’ve finished reading the advanced extra chapters in the wiki, you’ll have a nice frontend framework of your own—one that you can try in your next project.

Build your portfolio with it, or create that side project you’ve been thinking about for a while. When you use your framework in a medium-size project, you’ll see its shortcomings. Maybe it isn’t as convenient to use as the big frameworks, for example, or it may be missing some features. Reflect on what features you could add to fix those shortcomings and then try to implement them. The process will be challenging (and fun). You’ll want to open the source code of other frameworks, read their documentation, and debug their code, which is how you learn.

The GitHub repository’s discussions section is a forum where you can propose new features for the framework, ask questions, and get help from the community so that we can learn from one another. If you have a great idea for a feature but don’t know how to implement it, or if you have several options but want help from the community to evaluate its tradeoffs, this section is the place to go. You can find it at

As in any other public forum where people with all levels of experience participate and ask for directions, please be respectful and kind. I know that you are a kind person with a big heart and willingness to help, so I don’t think you need to be reminded of this point. Also, value other people’s time, and don’t expect them to do your work for you. Take the time to do your research and pose your questions in a way that makes it easy for others to help you. You can read more about these guide-lines in the “how to ask” section of Stack Overflow

).

After reading this book, completing the advanced topics in the wiki, and building a project with your framework, you can go a step further and try the following:

Innovate—Do you have an idea for a new feature that would make your framework stand out from the rest? What’s stopping you from implementing it?

Collaborate in the development of a well-established framework such as Vue, React, Angular, or Svelte—These projects are open source, and they’re always looking for new contributors. You’ll need to get acquainted with the codebase first, but you have many of the core concepts already covered.

330

CHAPTER 14

Testing asynchronous components

Improve the performance of your framework—Do you need inspiration? Check out Inferno JS (Start by measuring the performance of the operations in your framework; you might need to look for some tooling. Then find the bottlenecks and think about how to get rid of them.

Write another framework from scratch—Make all the decisions yourself, weigh the tradeoffs, and learn from your mistakes. You can always come back to this book for reference, but this time, you’ll be the one making the decisions.

I hope you’ve enjoyed reading this book as much as I enjoyed writing it. This chapter is a goodbye from me, but I hope to see you in the discussions section of the repository or the issues section if you find a bug in the code. I wish you all the best in your learning journey, in your professional career, and, above all, in your life.

Summary

 To test components with asynchronous hooks or event handlers, you need to await for the nextTick() function before checking what’s in the DOM.

 The nextTick() function returns a Promise that resolves when all the pending jobs in the scheduler have finished executing.

 To implement the nextTick() function, you need to schedule a task whose callback resolves the Promise returned by the nextTick() function.

appendix

Setting up the project

Before you write any code, you need to have an NPM project set up. In this appendix, I’ll help you create and configure a project to write the code for your framework.

I understand that you might not configure an NPM project from scratch with a bundler, a linter, and a test library, as most frameworks come with a command-line interface (CLI) tool (such as create-react-app or angular-cli) that creates the scaffolding and generates the boilerplate code structure. So I’ll give you two options to create your project:

Use the CLI tool I created for this book. With a single command, you can create a project and can start writing your code right away.

Configure the project from scratch. This option is more laborious, but it teaches you how to configure the project.

If you want to get started with the book quickly and can’t wait to write your awe-some frontend framework, I recommend that you use the CLI tool. In this case, you need to read section A.8. But if you’re the kind of developer who enjoys configuring everything from scratch, you can follow the steps in section A.9 to configure the project yourself.

Before you start configuring your project, I’ll cover some basics: where you can find the code for the book and the technologies you’ll use. These sections are optional, but I recommend that you read them before you start configuring your project. If you can’t wait to start writing code, you can skip them and move on to sections A.8 and A.9. You can always come back and read the rest of the appendix later.

331

332

APPENDIX

Setting up the project

A.1

Where to find the source code

The source code for the book is publicly available at I recommend that you clone the repository or download the zip archive of the repository so you can follow along with the book. The archive contains all the listings that appear in the book (in the listings folder), sorted by chapter. I’m assuming that you’re familiar with Git and know how to clone a repository and checkout tags or branches. If the previous sentence looks like nonsense to you, don’t worry; you can still follow along.

Download the project as a zip archive by clicking the <> Code button in the repository and then clicking the Download ZIP button.

WARNING

The code you’ll find in the repository is the final version of the framework—the one you’ll have written by the end of the book. If you want to check the code for each chapter, you need to check out the corresponding Git tag, as explained in the next section. You can also refer to the listings directory in the repository, which contains all the listings from the book sorted by chapter.

A.1.1

Checking out the code for each chapter

I tagged the source code with the number of each chapter where a new version of the framework is available. The tag name follows the pattern chX, where X is the number of the chapter. The first working version of the framework appears in chapter 6, for example, so the corresponding code can be checked out with the following Git command: $ git checkout ch6

Then, in chapters 7 and 8, you implement the reconciliation algorithm, resulting in a new version of the framework with enhanced performance. The corresponding code can be checked out as follows:

$ git checkout ch8

By checking out the code for each chapter, you can see how the framework evolves as we add features. You can also compare your code by the end of each chapter with the code in the repository. Note that not all chapters have a tag in the repository—only those that have a working version of the framework. I also wrote unit tests for the framework that I won’t cover in the book, but you can look at them to find ideas for testing your code.

NOTE

If you’re not familiar with the concept of Git tags, you can learn about them at . For this book, I assume that you have basic Git knowledge.

I recommend that you avoid copying and pasting the code from the book; instead, type it yourself. If you get stuck because your code doesn’t seem to work, look at the code in the repository, try to figure out the differences, and then fix the problem

A.1

Where to find the source code

333

yourself. If you write a unit test to reproduce the problem, so much the better. I know that this approach is more cumbersome, but it’s the best way to learn, and it’s how you’d probably want to tackle a real-world bug in your code. You can also refer to the listings/ directory in the repository, which contains all the listings from the book sorted by chapter.

You can find instructions for running and working with the code in the README

file of the repository (). This file contains up-to-date documentation on everything you need to know about the code. Make sure to read it before you work with the code. (When you start working with the code in an open source repository, reading the README file is the first thing you should do.)

A.1.2

A note on the code

I didn’t write the code to be the most performant and safest code possible. Many code snippets could use better error handling, or I could have done things in a more performant way by applying optimizations. But I emphasized writing code that’s easy to understand, clean, and concise—code that you read and immediately understand.

I pruned all the unnecessary details that would make code harder to understand and left in only the essence of the concept I want to teach. I went to great lengths to simplify the code as much as possible, and I hope you’ll find it easy to follow. But bear in mind that the code you’ll find in the repository and the book was written to teach a concept efficiently, not to be production-ready.

A.1.3

Reporting problems in the code

As many tests as I wrote and as many times as I reviewed the code, I’m sure that it still has bugs. Frontend frameworks are complex beasts, and it’s hard to get everything right. If you find a bug in the code, please report it in the repository by opening a new problem.

To open a new problem, go to the issues tab of the repository (

), and click the New Issue button. In the Bug Report row, click the Get Started button. Fill in the form with details on the bug you found, and click the Submit New Issue button.

You’ll need to provide enough information that I can easily reproduce and fix the bug. I know that it takes time to file a bug report with this many details, but it’s considered to be good etiquette in the open source community. Filing a detailed report shows respect to the people who maintain a project, who use their free time to create and maintain it for everyone to use free of charge. It’s good to get used to being a respectful citizen of the open source community.

A.1.4

Fixing a bug yourself

If you find a bug in the code and know how to fix it, you can open a pull request with the fix. Even better than opening a problem is offering a solution to it; that’s how open source projects work. You may also want to look at bugs that other people reported and

334

APPENDIX

Setting up the project

try to fix them. This practice is a great way to learn how to contribute to open source projects, and I’ll be forever grateful if you do so.

If you don’t know what GitHub pull requests are, I recommend that you read about them at Pull requests enable you to contribute to open source projects on GitHub, and also many software companies use them to add changes to their codebase, so it’s good to know how they work.

A.2

Solutions to the exercises

In most chapters of this book, I present exercises to test your understanding of the concepts I’ve explained. Some may seem to be straightforward, but others are challenging. If you want to absorb the concepts in the book, I recommend doing the exercises. Only if you get stuck or when you think you’ve found the solution should you check the answers. I’ve included the answers to all the exercises in the wiki of the repository:

In the wiki, you’ll find a page for the solutions to the exercises in the book by chapter:

The wiki includes extra chapters that cover advanced topics. These chapters didn’t make it into the book because the book would’ve been too long.

A.3

Advanced topics

When you finish this book, you’ll have a decent frontend framework—one that wouldn’t be suitable for large production applications but would work very well for small projects. The key is for you to learn how frontend frameworks work and have fun in the process. But you may want to continue learning and improving your framework. So I’ve written some extra chapters on advanced topics, which you can find in the repository’s wiki (section A.2). In these chapters, you’ll learn to

 Add Typescript types to your framework bundle.

 Add support for server-side rendering.

 Write the templates in HTML and compile them to render functions.

 Include external content inside your components (slots).

 Create a browser extension to inspect the components of your application.

I hope that you enjoy these chapters, but you have to learn the basics first, so make sure that you finish this book with a solid understanding of the topics it covers.

A.4

Note on the technologies used

There are heated discussions in the frontend ecosystem about the best tools. You’ll hear things like “You should use Bun because it’s much faster than Node JS” and

A.4

Note on the technologies used

335

“Webpack is old now; you should start using Vite for all your projects.” Some people argue about which bundler is best or which linter and transpiler you should be using.

I find blog posts of the “top 10 reasons why you should stop using X” kind to be especially harmful. Apart from being obvious clickbait, they usually don’t help the frontend community, let alone junior developers who have a hard time understanding what set of tools they “should” be using.

A cautionary tale on getting overly attached to tools

I once worked for a startup that grew quickly but wasn’t doing well. Very few customers used our app, and every time we added something to the code, we introduced new bugs. (Code quality wasn’t a priority; speed was. Automated testing was nowhere to be found.) Surprisingly, we blamed the problem on the tools we were using. We were convinced that when we migrated the code to the newest version of the framework, we got more customers, and things worked well from then on. Yes, I know that belief sounds ridiculous now, but we made it to “modernize” the tooling.

Guess what? The code was still the same: a hard-to-maintain mess that broke if you stared at it too long. It turns out that using modern tools doesn’t make code better if the code is poorly written in the first place.

What do I want to say here? I believe that your time is better used writing quality code than arguing about what tools will make your application successful. Well-architected and modularized code with a great suite of automated tests that, apart from making sure that the code works and can be refactored safely, also serves as documentation, beats any tooling. That isn’t to say that the tools don’t matter because they obviously do. Ask a carpenter whether they can be productive using a rusty hammer or a blunt saw.

For this book, I tried to choose tools that are mature and that most frontend developers are familiar with. Choosing the newest, shiniest, or trendiest tool wasn’t my goal. I want to teach you how frontend frameworks work, and the truth is that most tools work perfectly well for this purpose. The knowledge that I’ll give you in this book tran-scends the tools you choose to use. If you prefer a certain tool and have experience with it, feel free to use it instead of the ones I recommend here.

A.4.1

Package manager: NPM

We’ll use the NPM package manager () to create the project and run the scripts. If you’re more comfortable with yarn or pnpm, you can use them instead. These tools work very similarly, so you shouldn’t have any problem using the one you prefer.

We’ll use NPM workspaces (

, which were introduced in version 7. Make sure that you have at least version 7

installed:

$ npm --version

8.19.2

336

APPENDIX

Setting up the project

Both yarn and pnpm support workspaces, so you can use them as well. In fact, they introduced workspaces before NPM did.

A.4.2

Bundler: Rollup

To bundle the JavaScript code into a single file, we’ll use Rollup (), which is simple to use. If you prefer Webpack, Parcel, or Vite, however, you can use it instead. You’ll have to make sure that you configure the tool to output a single ESM

file (as you’ll see).

A.4.3

Linter: ESLint

To lint the code, we’ll use ESLint (a popular linter that’s easy to configure. I’m a firm believer that static analysis tools are musts for any serious project in which code quality is important (as it always is).

ESLint prevents us from declaring unused variables, having unused imports, and doing many other things that cause problems in code. ESLint is super-configurable, but most developers are happy with the default configuration, which is a good starting point. We’ll use the default configuration in this book as well, but you can always change it to your liking. If you deem a particular linting rule to be important, you can add it to your configuration.

A.4.4

(Optional) Testing: Vitest

I won’t be showing unit tests for the code you’ll write in this book to keep its size reasonable, but if you check the source code of the framework I wrote for the book, you’ll see lots of them. (You can use them to better understand how the framework works. When they’re well written, tests are great sources of documentation.) You may want to write tests yourself to make sure that the framework works as expected and that you don’t break it when you make changes. Every serious project should be accompanied by tests, which serve as a safety net as well as documentation.

I’ve worked a lot with Jest which has been my go-to testing framework for a long time. But I recently started using Vitest and decided to stick with it because it’s orders of magnitude faster. Vitest’s API is similar to Jest’s, so you won’t have problems if you decide to use it as well. But if you want to use Jest, it’ll do fine.

A.4.5

Language: JavaScript

Saying that we’ll use JavaScript may seem to be obvious, but if I were writing the framework for myself, I’d use TypeScript without hesitation. TypeScript is fantastic for large projects: types tell you a lot about the code, and the compiler will help you catch bugs before you even run the code. (How many times have you accessed a property that didn’t exist in an object using JavaScript? Does TypeError sends shivers down your spine?) Nontyped languages are great for scripting, but for large projects, I recommend having a compiler as your companion.

A.6

Structure of the project

337

Why, then, am I using JavaScript for this book? The code tends to be shorter without types, and I want to teach you the principles of how frontend frameworks work—

principles that apply equally well to JavaScript and TypeScript. I prefer to use JavaScript because the code listings are shorter and I can get to the point faster, thus teaching you more efficiently.

As with the previous tools, if you feel strongly about using TypeScript, you can use it instead of JavaScript. You’ll need to figure out the types yourself, but that’s part of the fun, right? Also, don’t forget to set up the compilation step in your build process.

A.5

Read the docs

Explaining how to configure the tools you’ll use in detail, as well as how they work, is beyond the scope of this book. I want to encourage you to go to the tools’ websites or repository pages and read the documentation. One great aspect of frontend tools is that they tend to be extremely well documented. I’m convinced that the makers compete against one another to see which one has the best documentation.

If you’re not familiar with any of the tools you’ll be using, take some time to read the documentation, which will save you a lot of time in the long run. I’m a firm believer that developers should strive to understand the tools they use, not just use them blindly.

(That belief is what encouraged me to learn how frontend frameworks work in the first place, which explains why I’m writing this book.)

One of the most important lessons I’ve learned over the years is that taking time to read the documentation is a great investment. Reading the docs before you start using a new tool or library saves you the time you’d spend trying to figure out how to do something that the documentation explains in detail. The time you spend trying to figure things out and searching StackOverflow for answers, is (in my experience) usually greater than the time you’d have spent reading the documentation up front.

6 hours of debugging can save you 5 minutes of reading documentation

— Jakob (@jcsrb)Twitter

Be a good developer; read the docs. (Or at least ask ChatGPT a well-structured question.)

A.6

Structure of the project

Let’s briefly go over the structure of the project in which you’ll write the code for your framework. The framework that you’ll write in this book consists of a single package (NPM workspace): runtime. This package is the framework itself.

NOTE

The advanced chapters you can read in the repository’s wiki add more packages to the project. For this reason, you want to structure your project by using NPM workspaces. It may seem silly to use workspaces when the project consists of a single package, but this approach will make sense when you add packages to the project.

338

APPENDIX

Setting up the project

TIP

If you’re not familiar with NPM workspaces, you can read about them at

It’s important to understand how they work so that the structure of the project makes sense and you can follow the instructions.

The folder structure of the project is as follows:

examples/

packages/

└── runtime/

When you add packages (advanced chapters in the repository), they’ll be inside the packages directory as well:

examples/

packages/

├── compiler/

├── loader/

└── runtime/

Each package has its own package.json file, with its own dependencies, and is bundled separately. The packages are effectively three separate projects, but they’re all part of the same repository. This project structure is common these days; it’s called a monorepo. The packages you include will define the same scripts:

build—Builds the package, bundling all the JavaScript files into a single file, which is published to NPM.

test—Runs the automated tests in watch mode.

test:run—Runs the automated tests once.

lint—Runs the linter to detect problems in your code.

lint:fix—Runs the linter and automatically fixes the problems it can fix.

prepack—A special lifecycle script ) that runs before the package is published to NPM. It’s used to ensure that the package is built before being published.

WARNING

The aforementioned scripts are defined in each of the three pack-

ages’ package.json files (packages/runtime, for example), not in the root package.json file. Bear this fact in mind in case you decide to configure the project yourself because there will be two package.json files total, plus one package that you add to the project.

A.7

Finding a name for your framework

Before you create the project or write any code, you need a name for your framework.

Be creative! The name needs to be one that no one else is using in NPM (where you’ll be publishing your framework), so it must be original. If you’re not sure what to call it, you can simply call it <your name>-fe-fwk (your name followed by -fe-fwk). To make sure

A.9

Option B: Configuring the project from scratch

339

that the name is unique, check whether it’s available on npmjs.com (

) by adding it to the URL, as follows: www.npmjs.com/package/ <your framework name>

If the URL displays the 404—not found error page, nobody is using that name for a Node JS package yet, so you’re good to go.

NOTE

I’ll use the <fwk-name> placeholder to refer to the name of your framework in the sections that follow. Whenever you see <fwk-name> in a command, you should replace it with the name you chose for your framework.

Now let’s create the project. Remember that you have two options. If you want to configure things yourself, jump to section A.9. If you want to use the CLI tool to get started quickly, read section A.8.

A.8

Option A: Using the CLI tool

Using the CLI tool I created for the book is the fastest way to get started. This tool will save you the time it takes to create and configure the project from scratch, so you can start writing code right away. Open your terminal, move to the directory where you want the project to be created, and run the following command:

$ npx fe-fwk-cli init <fwk-name>

With npx, you don’t need to install the CLI tool locally; it will be downloaded and executed automatically. When the command finishes executing, it instructs you to cd in to the project directory and run npm install to install the dependencies: $ cd <fwk-name>

$ npm install

That’s it! Go to section A.10 to learn how to publish your framework to NPM.

A.9

Option B: Configuring the project from scratch

NOTE

If you created your project by using the CLI tool, you can skip this section.

To create the project yourself, first create a directory for it. On the command line, run the following command:

$ mkdir <fwk-name>

Then initialize an NPM project in that directory:

$ cd <fwk-name>

$ npm init -y

340

APPENDIX

Setting up the project

This command creates a package.json file in the directory. You need to make a few edits in the file. For one, you want to edit the description field to something like

"description": "A project to learn how to write a frontend framework"

Then you want to make this package private so that you don’t accidentally publish it to NPM (because you’ll publish the workspace packages to NPM, not the parent project):

"private": true

You also want to change the name of this package to <fwk-name>-project to prevent con-flicts with the name of your framework (which you’ll use to name the runtime package you create in section A.10). Every NPM package in the repository requires a unique name, and you want to reserve the name you chose for your framework for the runtime package. So append -project to the name field:

"name": "< fwk-name> -project"

Finally, add a workspaces field to the file, with an array of the directories where you’ll create the packages that comprise your project (so far, only the runtime package):

"workspaces": [

"packages/runtime"

]

Your package.json file should look similar to this:

{

"name": "< fwk-name>- project",

"version": "1.0.0",

"description": "A project to learn how to write a frontend framework",

"private": true,

"workspaces": [

"packages/runtime"

]

}

You may have other fields—author, license, and so on—but the ones in the preceding snippet are the ones that must be there. Next, let’s create a directory in your project where you can add example applications to test your framework.

A.9.1

The examples folder

Throughout the book, you’ll be improving your framework and adding features to it.

Each time you add a feature, you’ll want to test it by using it in example applications.

Here, you’ll configure a directory where you can add these applications and a script to serve them locally. Create an examples directory at the root of your project: $ mkdir examples

A.9

Option B: Configuring the project from scratch

341

TIP

While the examples folder remains empty—that is, before you write any example application—you may want to add a .gitkeep file to it so that Git picks up the directory and includes it in the repository. (Git doesn’t track empty directories.) As soon as you put a file in the directory, you can remove the .gitkeep file, but keeping it doesn’t hurt.

To keep the examples directory tidy, create a subdirectory for each chapter: examples/ch02, examples/ch03, and so on. Each subdirectory will contain the example applications using the framework resulting from the chapter, which allows you to see the framework become more powerful and easier to use. You don’t need to create the subdirectories now; you’ll create them as you need them in the book.

Now you need a script to serve the example applications. Your applications will consist of an entry HTML file, which loads other files, such as JavaScript files, CSS

files, and images. For the browser to load these files, you need to serve them from a web server. The http-server package is a simple web server that can serve the example applications. In the package.json file, add the following script:

"scripts": {

"serve:examples": "npx http-server . -o ./examples/"

}

NOTE

This script is the only one you need to add to the project root package.json file. All the other scripts will be added to the package.json files of the packages you’ll create in the following sections. You won’t add any other script to the root package.json file.

The project doesn’t require the http-server package to be installed, as you’re using npx to run it. The -o flag tells the server to open the browser and navigate to the specified directory—in the case of the preceding command, the examples directory. Your project should have the following structure so far:

<fwk-name> /

├── package.json

└── examples/

└── .gitkeep

Great. Now let’s create the three packages that will make up your framework code.

A.9.2

Creating the runtime package

As I’ve said, your framework will consist of a single package: runtime. In the advanced chapters published in the repository’s wiki (section A.2), you’ll add packages to the project, but for now, you’ll create only the runtime package.

Before you create the package, you need to create a directory where you’ll put it: the packages directory you specified in the workspaces field of the package.json file.

Make sure that your terminal is inside the project’s root directory by running the pwd command:

$ pwd

342

APPENDIX

Setting up the project

The output should be similar to this (where path/to/your/framework is the path to the directory where you created the project):

path/to/your/framework/ <fwk-name>

If your terminal isn’t in the project’s root directory, use the cd command to navigate to it. Then create a packages directory:

$ mkdir packages

THE RUNTIME PACKAGE

Let’s create the runtime package. First, cd into the packages folder: $ cd packages

Then create the folder for the runtime package and cd into it:

$ mkdir runtime

$ cd runtime

Next, initialize an NPM project in the runtime folder:

$ npm init -y

Open the package.json file that was created for you, and make sure that the following fields have the right values (leaving the other fields as they are):

{

"name": " <fwk-name> ",

"version": "1.0.0",

"main": "dist/ <fwk-name> .js",

"files": [

"dist/ <fwk-name> .js"

]

}

Remember to replace <fwk-name> with the name of your framework. The main field specifies the entry point of the package, which is the file that will be loaded when you import the package. When you import code from this package as follows, import { something } from ' <fwk-name> '

JavaScript resolves the path to the file specified in the main field. You’ve told NPM that the file is the dist/< fwk-name>.js file, which is the bundled file containing all the code for the runtime package. Rollup will generate this file when you run the build script you’ll add to the package.json file in the next section.

The files field specifies the files that will be included in the package when you publish it to the NPM repository. The only file you want to include is the bundled file, so you’ve specified that in the files field. Files like README, LICENSE, and

A.9

Option B: Configuring the project from scratch

343

package.json are automatically included in the package, so you don’t need to include them. Your project should have the following structure (the parts in bold are the ones you just created):

<fwk-name> /

├── package.json

├── examples/

│ └── .gitkeep

└── packages/

└── runtime/

└── package.json

INSTALLING AND CONFIGURING ROLLUP

Let’s add Rollup to bundle the framework code. Rollup is the bundler that you’ll use to bundle your framework code; it’s simple to configure and use. To install Rollup, make sure that your terminal is inside the runtime package directory by running the pwd command:

$ pwd

The output should be similar to

path/to/your/framework/ <fwk-name> packages/runtime

If your terminal isn’t inside the packages/runtime directory, use the cd command to navigate to it. Then install the rollup package by running the following command: $ npm install --save-dev rollup

You also want to install two plugins for Rollup:

 rollup-plugin-cleanup—To remove comments from the generated bundle

 rollup-plugin-filesize—To display the size of the generated bundle Install these plugins with the following command:

$ npm install --save-dev rollup-plugin-cleanup rollup-plugin-filesize Now you need to configure Rollup. Create a rollup.config.mjs file in the runtime folder. (Note the .mjs extension, which tells Node JS that this file should be treated as an ES module and thus can use the import syntax.) Then add the following code: import cleanup from 'rollup-plugin-cleanup'

import filesize from 'rollup-plugin-filesize'

export default {

Entry point of the

input: 'src/index.js',

framework code

plugins: [cleanup()],

Removes comments from

output: [

the generated bundle

{

344

APPENDIX

Setting up the project

file: 'dist/ <fwk-name> .js',

Name of the

format: 'esm',

generated bundle

plugins: [filesize()],

},

Formats the bundle

],

Displays the size of the

as an ES module

}

generated bundle

The configuration, explained in plain English, means the following:

 The entry point of the framework code is the src/index.js file. Starting from this file, Rollup will bundle all the code that is imported from it.

 The comments in the source code should be removed from the generated bundle; they occupy space and aren’t necessary for the code to execute. The plugin-rollup-cleanup plugin does this job.

 The generated bundle should be an ES module (using import/export syntax, supported by all major browsers) that is saved in the dist folder as < fwk-name>.js.

 We want the size of the generated bundle to be displayed in the terminal so we can keep an eye on it and make sure that it doesn’t grow too much. The plugin-rollup-filesize plugin does this job.

Now add a script to the package.json file (the one inside the runtime package) to run Rollup and bundle all the code, and add another one to run it automatically before publishing the package:

"scripts": {

"prepack": "npm run build",

"build": "rollup -c"

}

The prepack script is a special script that NPM runs automatically before you publish the package (by running the npm publish command). This script makes sure that whenever you publish a new version of the package, the bundle is generated. The prepack script simply runs the build script, which in turn runs Rollup.

To run Rollup, call the rollup command and pass the -c flag to tell it to use the rollup.config.mjs file as configuration. (If you don’t pass a specific file to the -c flag, Rollup looks for a rollup.config.js or rollup.config.mjs file.)

Before you test the build command, you need to create the src/ folder and the src/index.js file with the following commands:

$ mkdir src

$ touch src/index.js

NOTE

In Windows Shell, touch won’t work; use call > filename instead.

Inside the src/index.js file, add the following code:

console.log('This will soon be a frontend framework!')

A.9

Option B: Configuring the project from scratch

345

Now run the build command to bundle the code:

$ npm run build

This command processes all the files imported from the src/index.js file (none at the moment) and bundles them into the dist folder. The output in your terminal should look similar to the following:

src/index.js → dist/ <fwk-name> .js...

┌─────────────────────────────────────┐

│ │

│ Destination: dist/ <fwk-name> .js │

│ Bundle Size: 56 B │

│ Minified Size: 55 B │

│ Gzipped Size: 73 B │

│ │

└─────────────────────────────────────┘

created dist/ <fwk-name> .js in 62ms

Recall that instead of <fwk-name> , you should use the name of your framework. That rectangle in the middle of the output is the rollup-plugin-filesize plugin in action. It’s reporting the size of the generated bundle (56 bytes, in this case), as well as the size it would be if the file were minified and gzipped. We won’t be minifying the code for this book, as the framework is going to be small, but for a production-ready framework, you should do that. A browser can load minified JavaScript faster and thus improve the Time to Interactive (TTI; metric of the web application that uses it.

A new file, dist/< fwk-name>.js, has been created in the dist folder. If you open it, you’ll see that it contains only the console.log() statement you added to the src/

index.js file.

Great! Let’s install and configure ESLint.

INSTALLING AND CONFIGURING ESLINT

ESLint is a linter that helps you write better code; it analyzes the code you write and reports any errors or potential problems. To install ESLint, run the following command: $ npm install --save-dev eslint

You’ll use the ESLint recommended configuration, which is a set of rules that ESLint enforces in your code. Create the .eslintrc.js file inside the packages/runtime directory with the following content:

module.exports = {

env: {

browser: true,

es2021: true,

},

extends: 'eslint:recommended',

overrides: [],

346

APPENDIX

Setting up the project

parserOptions: {

ecmaVersion: 'latest',

sourceType: 'module',

},

rules: {},

}

Finally, add the following two scripts to the package.json file:

"scripts": {

"prepack": "npm run build",

"build": "rollup -c",

"lint": "eslint src",

"lint:fix": "eslint src --fix"

}

You can run the lint script to check the code for errors and the lint:fix script to fix some of them automatically—those that ESLint knows how to fix. I won’t show the output of the lint script in the book, but you can run it yourself to see what it reports as you write your code.

INSTALLING VITEST (OPTIONAL)

As I’ve mentioned, I used a lot of tests while developing the code for this book, but I won’t be showing them in the book. You can find them in the source code of the framework and try to write your own as you follow along. Having tests in your code will help you ensure that things work as expected as you move forward with the development of your framework. We’ll use Vitest to run the tests. To install Vitest, run the following command:

$ npm install --save-dev vitest

Tests can run in different environments, such as Node JS, JSDOM, or a real browser.

Because we’ll use the Document API to create Document Object Model (DOM) elements, we’ll use JSDOM as the environment. (If you want to know more about JSDOM, see its repository: .) To install JSDOM, run the following command:

$ npm install --save-dev jsdom

With Vitest and JSDOM installed, you can create the vitest.config.js file with the following content:

import { defineConfig } from 'vitest/config'

export default defineConfig({

test: {

reporters: 'verbose',

environment: 'jsdom',

},

})

A.9

Option B: Configuring the project from scratch

347

This configuration tells Vitest to use the JSDOM environment and the verbose reporter, which outputs a descriptive report of the tests.

You should place your test files inside the src/__tests__ folder and name them

*.test.js so that Vitest can find them. Create the src/__tests__ folder: $ mkdir src/__tests__

Inside, create a sample.test.js file with the following content:

import { expect, test } from 'vitest'

test('sample test', () => {

expect(1).toBe(1)

})

Now add the following two scripts to the package.json file:

"scripts": {

"prepack": "npm run build",

"build": "rollup -c",

"lint": "eslint src",

"lint:fix": "eslint src --fix",

"test": "vitest",

"test:run": "vitest run"

}

The test script runs the tests in watch mode, and the test:run script runs the tests once and exits. Tests in watch mode are handy when you’re working with test-driven development, as they run every time you change the test file or the code you’re testing. Try the tests by running the following command:

$ npm run test:run

You should see the following output:

✓ src/__tests__/sample.test.js (1)

✓ sample test

Test Files 1 passed (1)

Tests 1 passed (1)

Start at 15:50:11

Duration 1.47s (transform 436ms, setup 1ms, collect 20ms, tests 3ms) You can run individual tests by passing the name or path of the test file as an argument to the test:run and test scripts. All the tests that match the name or path will be run. To run the sample.test.js test, for example, issue the following command: $ npm run test:run sample

This command runs only the sample.test.js test and produces the following output:

348

APPENDIX

Setting up the project

✓ src/__tests__/sample.test.js (1)

✓ sample test

Test Files 1 passed (1)

Tests 1 passed (1)

Start at 15:55:42

Duration 1.28s (transform 429ms, setup 0ms, collect 32ms, tests 3ms) Your project structure should look like the following at this point (the files and folders that you’ve created are in bold):

<fwk-name> /

├── package.json

├── examples/

│ └── .gitkeep

└── packages/

└── runtime/

├── package.json

├── rollup.config.mjs

├── .eslintrc.js

├── vitest.config.js

└── src/

├── index.js

└── __tests__/

└── sample.test.js

Your runtime package is ready for you, and you can start writing code. Next, let’s see how you can publish it to NPM.

A.10

Publishing your framework to NPM

As exciting as it is to develop your own framework, it’s even more exciting to share it with the world. NPM allows us to ship a package with one simple command: $ npm publish

But first, you need to create an account on NPM and log in to it from the command line. If you don’t plan to publish your framework, you can skip the next section.

A.10.1 Creating an NPM account

To create an NPM account, go to and fill out the form. It’s simple, and it’s free.

A.10.2 Logging in to NPM

To log in to NPM in your terminal, run the command

$ npm login

and follow the prompts. To make sure that you’re logged in, run the following command: $ npm whoami

A.10

Publishing your framework to NPM

349

You should see your username printed in the terminal.

A.10.3 Publishing your framework

To publish your framework, you need to make sure that your terminal is in the right directory, inside packages/runtime:

$ cd packages/runtime

Your terminal’s working directory should be packages/runtime (in bold):

<fwk-name> /

├── package.json

├── examples/

│ └── .gitkeep

Your terminal’s

working directory

└── packages/

should be here.

├── runtime/

├── compiler/

└── loader/

WARNING

Make sure that you’re not in the root folder of the project. The root folder’s package.json file has the private field set to true, which means that it’s not meant to be published to NPM. The runtime package is what you want to publish to NPM.

NOTE

Remember that the runtime package is the code for the framework (what you want to publish), and the root of the project is the monorepo containing the runtime and other packages (not to be published to NPM).

With your terminal in the runtime directory, run the following command: $ npm publish

You may wonder what gets published to NPM. Some files in your project always get published: package.json, README.md, and LICENSE. You can specify which files to publish in the files field of the package.json file. If you recall, the files field in the package.json file of the runtime package looks like this:

"files": [

"dist/ <fwk-name> .js"

],

You’re publishing only the dist/< fwk-name>.js file, the bundled version of your framework. The source files inside the src/ folder are not published.

The package is published with the version specified in the package.json file’s version field. Bear in mind that you can’t publish a package with the same version twice. Throughout the book, we’ll increment the version number every time we publish a new version of the framework.

350

APPENDIX

Setting up the project

A.11

Using a CDN to import the framework

After you’ve published your framework to NPM, you can create a Node JS project and install it as a dependency:

$ npm install <fwk-name>

This approach is great if you plan to set up an NPM project with a bundler (such as Rollup or Webpack) configured to bundle your application and the framework together, which you typically do when you’re building a production application with a frontend framework. But for small projects or quick experiments that use only a few files—like those you’ll work on in this book—it’s simpler to import the framework directly from the dist/ directory in your project or from a content-delivery network (CDN).

One free CDN that you can use is unpkg Everything that’s published to NPM is also available on unpkg.com, so after you’ve published your framework to NPM, you can import it from there:

import { createApp, h } from 'https://unpkg.com/ <fwk-name> '

If you browse to you’ll see the dist/< fwk-name>.js file you published in NPM, which the CDN serves to your browser when you import the framework from there. If you don’t specify a version, the CDN serves the latest version of the package. If you want to use a specific version of your framework, you can specify it in the URL:

import { createApp, h } from 'https://unpkg.com/ <fwk-name> @1.0.0'

Or if you want to use the last minor version of the framework, you can request a major version:

import { createApp, h } from 'https://unpkg.com/ <fwk-name> @1'

I recommend that you use unpkg with the versioned URL so you don’t have to worry about changes in the framework that might break your examples as you publish new versions. If you import the framework directly from the dist folder in your project, every time you bundle a new version that’s not backward-compatible, your example applications will break. You could overcome this problem by instructing Rollup to include the framework version in the bundled filename, but I’ll be using unpkg in this book.

You’re all set! It’s time to start developing your framework. You can turn to chapter 2 to start the exciting adventure that’s ahead of you.

index

Symbols

areNodesEqual() function , ,

arguments object

!= operator

arrayDiffSequence() function

=== operator

#array property

== operator

arrays, diffing

arraysDiff() function

Numerics

arraysDiffSequence() function

adding item to old array

404 error page

addition case

move case

A

noop case

remove case

action attribute

removing outstanding items

add() method

TODO comments

addedListeners variable

ArrayWithOriginalIndices class

addEventHandler() method

addEventListener() function , async/await

asynchronous components, testing

addEventListener() method

nextTick() function

addItem() method

fundamentals behind

addition case

implementing

add operation

testing components with asynchronous

addProps() function

onMounted() hook

addTodo() function ,

publishing version 4.1 of framework

after-command handlers

attributes object

afterEveryCommand() function

attrs property

afterEveryCommand() method

await function

App() component

await keyword

App() function

append() method

B

application, mounting

application instance

BarComponent

.apply() method

<body> tag, cleaning up

applyArraysDiffSequence() function

browser side of SPAs

351

352

INDEX

browser side of SSR applications

updating state and patching DOM

btn class

omponents; stateless compo-btnClicked event

nents

build command

conditional rendering

build script

element nodes

<button> element

mapping strings to text nodes

null values, removing

C

console.log() function

console.log() statement

call() function

consumer function

call stack

Counter component

catch() function

count property

cd command

count state property

CDN (content-delivery network)

createApp() function ,

-c flag

ch8 label

createComponentNode() function , checkWinner() function

child nodes, patching

created state

childNodes array

createElement() function

children array

createElement() method

children property

createElementNode() function ,

class attribute

classList property

createFragmentNodes() function

className property

class property

createTextNode() function

click event

createTextNode() method

click event handler

CreateTodo() component –

CLI (command-line interface)

CreateTodo() function

color property

CSS (Cascading Style Sheets), loading files

commands

CSS class, patching

Component class , CSSStyleDeclaration object

cssText property

COMPONENT constant

component lifecycle hooks

D

event loop cycle

scheduler

dblclick event

component methods

debounce() function

component nodes, equality

declarative code

Component prototype

defineComponent() function

components

adding as new virtual DOM type

and components with state

mounting component virtual nodes

basic component methods

updating elements getter

checking whether component is already

as classes

mounted

as function of state

described

binding event handlers to

description field

composing views

destroyDOM() function

keeping track of mounting state of

offset

destroying element

overview of

destroying fragment

patching DOM using component’s offset

destroying virtual DOM

framework building and publishing

INDEX

353

destroyDOM() method

E

destroyed state

developers, frontend frameworks

ECMAScript (ES) modules

diffSeq

Element class

directives

element nodes

disabled attribute

extending solution to

disabled key

mapping strings to text nodes

:disabled pseudoclass

null values, removing

dispatch() function

elements, destroying

dispatch() method

elements getter

dispatcher

updating

assigning handlers to commands

el property

dispatching commands

el reference

notifying renderer about state changes

emit() function

Dispatcher class –

emit() method

:enabled pseudoclass

dispatcher property

enqueueJob() function

dispatcher variable

eqeqeq rule

div element

equalsFn parameter

Document API

ES (ECMAScript) modules

document.createDocumentFragment()

ESLint, installing and configuring

method

eslint-plugin-no-floating-promise plugin

document.createElement() function

event handlers

document.createElement() method

binding to components

document.createTextNode() method

context

DocumentFragment class

#eventHandlers object

document.getElementById() function

#eventHandlers property

DOM (Document Object Model)

event listeners, patching

event loop

destroying

event loop cycle

elements

scheduler

fragments

events

text nodes

communication via, patching component virtual

element nodes

nodes

keyed lists, extending solution to element

emitting events

nodes

extracting props and events for component

mounting

saving event handlers inside component

element nodes

fragment nodes

vs. callbacks

insert() function

vs. commands

text nodes

wiring event handlers

with host component

EventTarget interface

patching

execution stack

with host component

extractPropsAndEvents() function , separating concerns

virtual DOM, mounting

DOMTokenList object

F

DOM_TYPES.COMPONENT

DOM_TYPES constant

fastDeepEqual() function

DOM_TYPES object

fast-deep-equal library

doWork() function

fetch() function

files field

354

INDEX

findIndexFrom() method

handlers, assigning to commands

flatMap() method

hasOwnProperty() function

flushPromises() function

hasOwnProperty() method

FooComponent

Header component

Footer component

hFragment() function

for loop

higher-level application code

fragment nodes

hostComponent

implementing

host component

fragments, destroying

mounting DOM with

frameworks

patching DOM with

assembling state manager into

hostComponent argument

application instance

hostComponent parameter

application instance’s renderer

hostComponent reference

application instance’s state manager

hostElement reference

components dispatching commands

hString() function

unmounting application

HTMLElement class

building and publishing

HTMLElement instance

component lifecycle hooks and scheduler

HTML (Hypertext Markup Language)

finding name for

hydrating pages

frontend web framework, lifecycle hooks

loading files

loading pages

importing with CDN

HTML markup

publishing

HTMLParagraphElement class

short example

HTMLUnknownElement

publishing to NPM

http-server package

creating NPM account

hydration

logging in to NPM

HYPERLINK

publishing framework

hyperscript() function

publishing version 4 of

from property

I

frontend frameworks

building

id attribute

features

id property

implementation plan

imperative code

finding name for

importing framework, with CDN

overview of

import statement

browser and server sides of SSR

indexOf() function

applications

index parameter

browser side of SPAs

index property

developer’s side

initializing views

reasons for building

<input> element

orks

input event

insert() function

G

insertBefore() method

isAddition() method

global application state

#isMounted property

Greetings component

isNoop() method

isRemoval() method

H

isScheduled Boolean variable

isScheduled flag

h() function

item property

items prop

INDEX

355

J

List component

listeners object

JavaScript, loading files

listeners property

JavaScript code

ListItem component

adding, removing, and updating to-dos

loading state

initializing views

loadMore() method

rendering to-dos in edit mode

low-level operation

rendering to-dos in read mode

Jest

M

job() function

json() method

Main component

main field

K

mapTextNodes() function

max key

key attribute

MessageComponent() function

component nodes equality

methods

removing from props object

binding event handlers to components

using

keydown event

component

keyed lists

component methods

application instance

mounting DOM with host component

extending solution to element nodes

microtasks

minified files

key attribute

min key

component nodes equality

monorepo

removing from props object

mount() function

using

mount() method , using index as key

using same key for different elements

mountDOM() function ,

key prop

changes in rendering

component lifecycle

L

mountDOM() example

mounting component virtual nodes

<label> element

mounting virtual DOM

lazy-loading

scheduler ,

lexically scoped

scheduling lifecycle hooks execution

libraries

with host component

li elements

mounted state

lifecycle hooks

move case

asynchronicity

moveItem() method

execution context

move operation

mounted and unmounted

MyFancyButton component

overview

scheduler

N

simple solution that doesn’t quite work

name attribute

tasks, microtasks, and event loop

NameComponent component

scheduling execution –

name field

lifecycle of application

name prop

lint script

name property

lipsum() function

newKeys set

356

INDEX

newNode object

onUnmounted() lifecycle hook

nextTick() function

dealing with asynchronicity and execution

fundamentals behind

context

implementing

hooks asynchronicity

testing components with asynchronous

hooks execution context

onMounted() hook

onUpdated() lifecycle hook

ngAfterViewInit() lifecycle hook

op property

ngOnChanges() lifecycle hook

originalIndexAt() method

ngOnDestroy() lifecycle hook

originalIndex property

ngOnInit() lifecycle hook

#originalIndices property

nodes, types of

none state

P

noop case

noopItem() method

paint flashing

noop operation

ParentComponent

npm install command

parentEl instance

NPM (Node Package Manager)

parentEl variable

creating NPM account

#patch() method ,

logging in to NPM

patch() method

publishing framework

patchAttrs() function

npm publish command

patchChildren() function

npm run build script

patchChildren() subfunction

null values, removing

patchClasses() function

patchComponent() function

O

patchDOM() function

objects, diffing

adding component case to

objectsDiff() function

and changes in rendering mechanism

offset property

offsets –

passing component’s instance to

-o flag

reconciliation algorithm

onBeforeMount() lifecycle hook

updating state and patching DOM

onBeforeUnmount() lifecycle hook

patchElement() function

onBeforeUpdate() lifecycle hook

patches

onclick event handler

patchEvents() function

onclick event listener

patchStyles() function

oninput event

patchText() function

onMounted() callback

plugins array

onMounted() function

prepack script

onMounted() hook , processJobs() function ,

testing components with

process.nextTick() function

onMounted() lifecycle hook

project, setting up

dealing with asynchronicity and execution

advanced topics

context

configuring from scratch

hooks asynchronicity

examples folder

hooks execution context

runtime package –

on object

ESLint

on property

finding name for framework

onUnmounted() function

NPM package manager

onUnmounted() hook , publishing framework to NPM

Rollup

INDEX

357

project, setting up (continued)

reducers property

source code

remove() method

checking out code for each chapter

removeAttribute() method

fixing bugs

remove case

notes on

removeElementNode() function

reporting problems in

removeEventListener() method

using CDN to import framework

removeEventListeners() function

Vitest

removeFragmentNodes() function

project structure

removeItem() method

Promise

removeItemsAfter() method

props object

removeItemsFrom() method

removing key attribute from

remove operation

props property

remove outstanding items

publishing

removeStyle() function

frameworks

removeTextNode() function

framework to NPM

removeTodo() function

publish NPM script

removeTodo() reducer function

pure function

remove-todo command

pwd command

render() function

render() method , Q

renderApp() function –

queueMicrotask() function

renderApp() method

renderer

R

renderInEditMode() function

renderTodoInEditMode() function

React.createElement() function

renderTodoInReadMode() function

reconciliation algorithm

replaceChild() method

changes in rendering

resolve() function

comparing virtual DOM trees

Rollup, installing and configuring

adding new <span> node

rollup command

applying changes

rollup package

finding differences

@rollup/plugin-commonjs

modifying class attribute and text content of

@rollup/plugin-node-resolve

first <p> node

routes

modifying class attribute and text content of

navigating among

second <p> node

navigating between

modifying id and style attributes of parent

runtime package

<div> node

creating

diffing virtual trees

installing and configuring Rollup

diffing arrays

installing Vitest

diffing arrays as sequence of operations

runtime package

mounting DOM

S

element nodes

fragment nodes

scheduler –

insert() function

functions of

text nodes

implementing

three key functions of

scheduling lifecycle hooks execution

reducer functions

simple solution that doesn’t quite work

reducers, refactoring TODOs application

tasks, microtasks, and event loop

reducers object

scheduleUpdate() function

358

INDEX

search event

components as classes

SearchField component

components with state

separation of concerns

updating state and patching DOM

sequence variable

stateless components

serve:examples script

state management

server side of SSR applications

state manager

setAttribute() function

assembling into framework

setAttributes() function ,

application instance

setClass() function

application instance’s renderer

setInterval() function

application instance’s state manager

setStyle() function

components dispatching commands

setTimeout() function

unmounting application

setting attributes

dispatcher

class attribute

assigning handlers to commands

rest of attributes

dispatching commands

style attribute

notifying renderer about state changes

setting up project CLI tool and

from JavaScript events to application domain

SFCs (single-file components)

commands

side effect

reducer functions

silent failure

state object

source code

state property ,

checking out code for each chapter

statically served files

fixing bugs

strings, mapping to text nodes

notes on

style, patching

reporting problems in

style attribute

span element

style property

span node

subcomponents

SPAs (single-page applications)

adding components as new virtual DOM

browser side of

type

creating application’s view

mounting component virtual nodes

handling user interactions

updating elements getter

loading HTML file

communication via events

loading JavaScript and CSS files

emitting events

navigating among routes

extracting props and events for

splice() method

component

SSR (server-side rendered) applications

saving event handlers inside component

browser and server sides of

handling user interactions

wiring event handlers

hydrating HTML page

communication via props and events,

loading HTML page

patching component virtual nodes

navigating between routes

state

subscribe() method

refactoring TODOs application

subscriptions array

virtual DOM as function of

subscriptions property

state() function

subs variable

state() method

SVG (Scalable Vector Graphics)

stateful components

switch statement

anatomy of

methods

T

properties

component’s offset –

<table> element

patching DOM using

tag argument

INDEX

359

tag key

U

tag property

tasks

UIs (user interfaces)

testing

<ul> element

asynchronous components

ul element

frontend frameworks

unmount() method

nextTick() function

unmounted lifecycle hooks

test:run script

dealing with asynchronicity and execution

test script

context

text content

hooks asynchronicity

textContent property

hooks execution context

text nodes

unpredictable reordering

destroying

update-current-todo event

mapping strings to

updateProps() method , mounting

Text type

updateState() function

then() callback

updateState() method

then() function

updateTodo() function

this keyword

useEffect() hook

this value

user interactions

tick() function

useState() hook

tick() lifecycle hook

TodoInEditMode() function

V

TodoInReadMode() function

TodoItem() component

value attribute

TodoItem() function

value property

TodoList() component

vanilla JavaScript

TodoList() function

writing application

to-dos

HTML markup

adding, removing, and updating

JavaScript code

rendering in edit mode

project setup

rendering in read mode

vdom node

TODOs application

vdom property

refactoring

vdom tree

cleaning up <body> tag

vdom variable

defining reducers

vdom (virtual DOM)

defining state

virtual DOM

defining views

view() function

writing application

view property

HTML markup

views

JavaScript code

adding, removing, and updating to-dos

project setup

composing

todos.js file

initializing

TodosList component

refactoring TODOs application

todosList element

rendering to-dos in edit mode

toSpliced() method

rendering to-dos in read mode

transition property

v-if directive

two-way binding

virtual DOM

type attribute

adding components as new type

typeof operator

mounting component virtual nodes

type property

typescript-eslint program

updating elements getter

360

INDEX

virtual DOM (continued)

mounting into DOM

components

patching component virtual nodes

as function of state

rendering optimization

composing views

virtual trees

overview of

diffing arrays

destroying

diffing arrays as sequence of operations

elements

fragments

algorithm for

text nodes

defining operations

element nodes

example by hand

mounting

implementing algorithm

adding event listeners

diffing objects

element nodes

Vitest

example of mountDOM() function

installing

fragment nodes

testing library

setting attributes

VM (virtual machine)

text nodes

virtual nodes into DOM

W

rendering

separating concerns

while loop

testing functions

Window object

types of nodes

#wireEventHandlers() method

virtual DOM trees

with host component

applying changes

withoutNulls() function

comparing

workspaces field

finding differences between

virtual nodes

Z

destroying component virtual nodes

equality

zmountDOM() function

12. Framework changes the URL

without reloading the page.

1. User requests page.

/abc

2. Server responds with (mostly empty)

HTML file.

6. User interacts with the

html

page. An event is emitted.

3. Browser loads .js and .css files.

5. Framework creates the

HTML programmatically.

9. User clicks a link. A

navigation event is emitted.

js

css

Application bundle

8. Framework patches the parts

of HTML that change.

11. Framework replaces the

current view with the new one.

4. Framework reads the

view’s components.

js

Vendors bundle

7. Framework executes the event

handlers defined in the application.

10. Framework’s router selects the

view that needs to be rendered.

This diagram illustrates the operational flow of a single-page application (SPA), starting from the initial HTML

document load, followed by script execution that programmatically constructs the DOM. It further delineates how user interactions lead to dynamic DOM updates by the framework and demonstrates the framework’s role in handling routing changes by patching the DOM, thereby eliminating the need for server requests for new pages upon navigating to different routes.

SOFTWARE DEVELOPMENT

“If you’re looking for a

BUILD A Frontend Web Framework (FROM SCRATCH)

project to really explore

JavaScript and web

Ángel Sola Orbaiceta

technology—this book might

You use frontend frameworks every day, but do you really be just the thing. It delivers know what’s going on behind the API? Building your

an excellent overview of the

own framework is a great way to learn how they inter-

nuts and bolts.

act with the DOM, generate page views, route data between

—Eric Elliott ”

components, and communicate with the underlying operating

system. With this interesting and entertaining book, you’ll

Engineering Manager, Adobe

build your own web framework step-by-step in JavaScript,

ready to share with the world as an NPM package!

“You will understand and

learn how to create a frame-

Build a Frontend Web Framework (From Scratch) guides you

work, better master your

through a simple component-based frontend framework that

current framework, and

borrows from React, Svelte, Angular, and other familiar tools.

become an even more

You’ll learn how a modern framework operates by adding fea-

qualifi ed professional.

tures like component state and lifecycle management, a virtual

Prepare your environment

DOM, and reconciliation algorithms to update the HTML

effi

ciently. You’ll appreciate how each critical concept is

and enjoy the reading!

—Mayk Brito

broken down into easy-to-digest chunks and explained with

engaging graphics.

Chief Content Offi

cer, Rocketseat

What’s Inside

“Extraordinarily educational

and fascinating. Th

e best path

● Create HTML documents programmatically

to understanding how

● Defi ne the view with the virtual DOM

frameworks work

● Implement a component lifecycle scheduler

under the hood.

—Rod Weis, CodeTh ”

For web developers familiar with JavaScript and Node.

eWeb.ca

Ángel Sola Orbaiceta has worked in the software industry for

over a decade, creating software for the cloud, macOS, and

Windows desktop applications.

See first page

For print book owners, all ebook formats are free:

https://www.manning.com/freebook

ISBN-13: 978-1-63343-806-4

M A N N I N G

Назад: 11.2.4 Emitting events
На главную: Предисловие