My translator is a front-end engineer of the Strange Dance Company
What Web Frameworks Solve And How To Do Without Them
Noam Rosenthal
The original address: www.smashingmagazine.com/2022/01/web…
The first chapter
The profile
In this article, Noam Rosenthal takes an in-depth look at some of the technical features common across frameworks and explains how different frameworks implement them and how much they cost.
Author’s brief introduction
Noam Rosenthal is a WEB platform consultant, WebKit & Chromium contributor, technical writer, and an experienced WEB developer. His focus is on bringing WEB development and browser/standards development closer together.
background
I’ve recently become very interested in comparing frameworks to normal JavaScript. It started with some frustration I had with React in some projects, and my recent deeper understanding of Web standards as a specification editor.
I’m interested in what the similarities and differences are between these frameworks, what the Web platform should offer as a streamlined alternative, and whether it’s adequate. My goal is not to bash frameworks, but to understand their costs and benefits, determine if alternatives exist, and see if we can learn from them even if we do decide to use them.
In part 1, I’ll delve into some of the technical features common across frameworks and how they are implemented by different frameworks. I’ll also discuss the costs of using these frameworks.
The framework
I chose four frameworks to study :React, which is the dominant framework today, and three new competitors that claim to work differently from React.
- React
“React makes it easy to create interactive UIs. Declarative views make code more predictable and easier to debug.”
- SolidJS
“Solid follows the same philosophy as React… But it has a completely different implementation that does away with the virtual DOM.”
- Svelte
“Svelte is a whole new way of building user interfaces… When you build an application, it’s a compilation step. Instead of using techniques like virtual DOM differentiation, Svelte writes code that surgically updates the DOM when application state changes.”
- Lit
“Building on the Web Components standard, Lit adds… Responsive, declarative templates and some thoughtful features.”
To summarize the differences between these frameworks:
- React makes building uIs easier with declarative views.
- SolidJS follows the React philosophy, but uses a different technology.
- Svelte uses a compilation method for the UI.
- Lit uses existing standards and adds some lightweight features.
What problems does the framework solve
The framework itself mentions declarative, reactive, and virtual DOM. Let’s explore what that means.
Declarative programming
Declarative programming is an paradigm for defining logic without specifying control flow. We describe what the outcome needs to be, not what steps we need to take to get there.
In the early days of declarative frameworks, circa 2010, DOM apis were much simpler and wordy, and writing Web applications in emergent JavaScript required a lot of boilerplate code. At this point, the model-View-View model (MVVM) concept became popular, and the then groundbreaking Knockout and AngularJS frameworks provided a JavaScript declaration layer to handle the complexity in the library.
MVVM is not a widely used term now, and is somewhat of a variation on the old term “data binding.”
Data binding
Data binding is a declarative way of expressing how data is synchronized between the model and the user interface.
All the popular UI frameworks provide some form of data binding, and they all start their tutorials with a data binding example.
Here is the data binding in JSX (SolidJS and React):
function HelloWorld() {
const name = "Solid or React";
return (
<div>Hello {name}!</div>)}Copy the code
Data binding in Lit:
class HelloWorld extends LitElement {
@property()
name = 'lit';
render() {
return html`<p>Hello The ${this.name}!</p>`; }}Copy the code
Data binding in Svelte:
<script> let name = 'world'; </script> <h1>Hello {name}! </h1>Copy the code
responsive
Reactive is a declarative way of expressing the propagation of change.
When we have a way to express data binding declaratively, we need an efficient way for the framework to propagate changes.
The React engine compares the rendered result to the previous one and applies the difference to the DOM itself. This method of handling change propagation is called the virtual DOM.
In SolidJS, this is done more explicitly with its store and built-in elements. For example, the Show element tracks internal changes, not the virtual DOM.
In Svelte, “reactive” code is generated. Svelte knows which events cause changes and generates simple code that draws a line between events and DOM changes.
In Lit, responsivity is done using element attributes, essentially relying on the built-in responsivity of HTML custom elements.
logic
When a framework provides declarative interfaces for data binding and implements responsiveness, it also needs to provide a way to express some logic that has traditionally been written in a deterministic fashion. The basic building blocks of logic are “if” and “for,” and all major frameworks provide some expression for these building blocks.
Conditional statement/flow control
In addition to binding basic data such as numbers and strings, each framework provides a “conditional” primitive. In React, it looks like this:
const [hasError, setHasError] = useState(false); return hasError ? <label>Message</label> : null; ... setHasError(true);Copy the code
SolidJS provides a built-in conditional component Show:
<Show when={state.error}>
<label>Message</label>
</Show>
Copy the code
Svelte provides the #if directive:
{#if state.error}
<label>Message</label>
{/if}
Copy the code
In Lit, you can use an explicit ternary operation in the rendering function:
render() {
return this.error ? html`<label>Message</label>`: null;
}
Copy the code
Lists
Another common framework primitive is list processing. Lists are key parts of the UI — contact lists, notification lists, and so on — and to work effectively, they need to be responsive, rather than updating the entire list as one data item changes.
In React, lists are handled like this:
contacts.map((contact, index) =>
<li key={index}>
{contact.name}
</li>)
Copy the code
React uses special key attributes to differentiate between list items and ensure that the entire list is not replaced each time it is rendered.
In SolidJS, for and index are used as built-in elements:
<For each={state.contacts}>
{contact => <DIV>{contact.name}</DIV> }
</For>
Copy the code
Internally, SolidJS uses its own repository and for and indexes to decide which elements to update when a project changes. It is more explicit than React and allows us to avoid the complexity of the virtual DOM.
Svelte uses the each directive to compile according to its updater:
{#each contacts as contact}
<div>{contact.name}</div>
{/each}
Copy the code
Lit provides a repeat function that works like React’s key list mapping:
repeat(contacts, contact => contact.id,
(contact, index) => html`<div>${contact.name}</div>`
Copy the code
A component model
One thing that is beyond the scope of this article is the component model in different frameworks and how to handle it using custom HTML elements.
Note: This is a big topic and I hope to cover it in a future post because this one is too long.
The cost of
The framework provides declarative data binding, control-flow primitives (conditions and lists), and response mechanisms to propagate changes.
They also provide other major functionality, such as ways to reuse components, but that is the subject of another article.
Is the framework useful? Yes. They give us all these convenient features. But is it the right question? Using frameworks comes at a cost. Let’s see what those costs are.
Package size
When looking at the packaged package size, I like to look at the compressed non-Gzip size. This is the size that most correlates with the CPU cost of JavaScript execution.
- ReactDOM is about 120 KB.
- SolidJS is about 18KB.
- Lit is about 16 KB.
- Svelte is about 2 KB, but the generated code varies in size.
Today’s frameworks seem to do a better job than React, keeping packages small. The virtual DOM requires a lot of JavaScript.
build
Somehow, we got used to “building” our web applications. To start a front-end project, you must first set up packaging tools like Node.js and Webpack, handle some configuration of babel-typescript, and so on.
The smaller the package size of the framework, the more expressive it is, and the greater the burden on build tools and translation time.
Svelte claims that the virtual DOM is pure overhead. I agree with that, but maybe “building” (like using Svelte and SolidJS) and customizing a client template engine (like using Lit) is also pure overhead and a different form of presentation?
debugging
There is some overhead and cost associated with building and compiling.
When we use or debug a Web application, the code we see is completely different from what we wrote. We now rely on special debugging tools of varying quality to reverse-engineer what happens on the site and relate it to errors in our own code.
In React, the call stack is never “yours” — React handles scheduling for you. This works well when there are no bugs. But try to identify the reason for the re-presentation of the infinite loop and you will experience a world of pain.
In Svelte, the package size of the library itself is small, but you need to publish and debug a bunch of arcane generated code, which is a responsive implementation of Svelte, customized to the needs of the application.
With Lit, it is build neutral, but to debug it effectively, you must understand its template engine. This is probably the biggest reason why I’m skeptical of frameworks.
When you look for custom declarative solutions, you end up with more painful imperative debugging. The examples in this document use Typescript as the API specification, but the code itself does not need to be compiled.
upgrade
In this document, I’ve covered four frameworks, but there are many more (AngularJS, ember.js, vue.js, etc.). Can you count on the framework, its developers, its popularity, and its ecosystem to serve you as it grows?
One thing more frustrating than fixing your own bugs is having to find workarounds for framework bugs. There is one thing more frustrating than framework bugs, and that is the bug that occurs when you upgrade to a new version of the framework without making any changes to the code.
True, this problem also exists in browsers, but when it does happen, it happens to everyone, and in most cases, a fix or released solution is imminent. In addition, most of the patterns in this document are based on mature Web platform apis; There is no need to always bleed to the edge.
summary
We took an in-depth look at the core issues that frameworks try to solve and how they solve them, focusing on data binding, responsiveness, conditions, and lists. We also looked at the cost.
In later sections, we’ll learn how to solve these problems without using frameworks at all, and what we can learn from them. Stay tuned!
Special thanks to everyone for their corrections :Yehonatan Daniv, Tom Bigelajzen, Benjamin Greenbaum, Nick Ribal and Louis Lazaris.
The second chapter
In part two, Noam presents some patterns of how to directly use the Web platform as an alternative to some of the solutions offered by the framework.
In the first chapter, we examined the different benefits and costs of using a framework from the point of view of the core issues it is trying to solve, focusing on declarative programming, data binding, responsiveness, lists, and conditions. Now, we’ll see if an alternative can emerge from the web platform itself.
Launch your own framework?
Exploring without a framework seems like the inevitable result of using your own framework for reactive data binding. After trying this approach before, and seeing how expensive it can be, I decided to follow a guiding principle in this quest; I wasn’t pushing my own framework, but I wanted to see if I could use the Web platform directly in a way that made frameworks less necessary. If you are considering using your own framework, please note that there is one component that is not covered in this article.
Common choice
The Web platform already provides a declarative programming mechanism out of the box :HTML and CSS. This mechanism is mature, well tested, popular, widely used, and documented. However, it does not provide explicit built-in concepts such as data binding, conditional rendering, and list synchronization, and responsiveness is a subtle detail of features across multiple platforms.
When I browse through the documentation for popular frameworks, I can go straight to the features described in Part 1. When I read the Web platform documentation (for example, on MDN), I found many confusing patterns of how to do things, with no data binding, list synchronization, or responsive conclusive representations. I’ll try to draw some guidelines for addressing these issues on a Web platform, without the need for a framework (in other words, by ordinary means).
Stable DOM trees and cascades
Let’s go back to the mislabeling example. In ReactJS and SolidJS, the declarative code we create is transformed into imperative code that either adds a label to the DOM or removes it. In Svelte, this code is generated.
But what if we didn’t have this code at all, and instead used CSS to hide and display error tags?
<style>
label.error { display: none; }
.app.has-error label.error {display: block; }
</style>
<label class="error">Message</label>
<script>
app.classList.toggle('has-error', true);
</script>
Copy the code
In this case, responsiveness is handled in the browser — the application’s changes to the class are propagated to its descendants until an internal mechanism in the browser decides whether to render the tag or not.
This technique has several advantages:
- The bundle size is 0.
- There are no build steps.
- In native browser code, change propagation is optimized and tested, and unnecessary and expensive DOM operations such as append and delete are avoided.
- The selector is stable. In this case, you can rely on the presence of the label element. You can animate them without relying on complex constructs such as “transition groups”. You can save a reference to it in JavaScript.
- If the tag is shown or hidden, you can see the reason in the styles panel of the developer tools, which shows you the entire cascade, and eventually the chain of rules in the tag is visible (or hidden).
Even if you read this article and choose to stick with frameworks, the idea of using CSS to keep the DOM stable and state changing is very powerful. Consider where this might be useful to you.
Forms-oriented “Data binding”
Before the era of javascript-heavy single-page applications (SPA), forms were the primary way to create Web applications that contained user input. Traditionally, the user would fill out the form and click the “Submit” button, and the server-side code would process the response. Forms are a multi-page version of a data-bound and interactive application. Without a doubt, HTML elements with basic input and output names are form elements.
Because of the widespread use and long history of the forms API, it has accumulated some hidden benefits that make them useful for problems that traditionally have been considered unsolvable by forms.
Forms and form elements as stable selectors
Forms can be accessed by name (using document.forms” document.forms”) and each form element can be accessed by name (using form.elements). In addition, you can access the form associated with the element (using form Attributes). This includes not only input elements, but other form elements such as Output, Textarea, and FieldSet, which allow nested access to elements in the tree.
In the error label example in the previous section, we showed how to show and hide error messages responsively. Here’s how we update the error message text in React (and SolidJS):
const [errorMessage, setErrorMessage] = useState(null);
return <label className="error">{errorMessage}</label>
Copy the code
When we have a stable DOM and stable tree form and form elements, we can do the following:
<form name="contactForm">
<fieldset name="email">
<output name="error"></output>
</fieldset>
</form>
<script>
function setErrorMessage(message) {
document.forms.contactForm.elements.email.elements.error.value = message;
}
</script>
Copy the code
It looks pretty wordy in its original form, but it’s also very stable, straightforward, and high-performance.
The input form
Typically, when we build spAs, we use some JSON-like API to update our server or whatever model we use.
Here’s a familiar example (written in Typescript for ease of reading):
interface Contact { id: string; name: string; email: string; subscriber: boolean; } function updateContact(contact: contact) {... }Copy the code
It is common in framework code to generate this Contact object by selecting input elements and constructing objects piece by piece. There is a neat alternative to using forms correctly:
<form name="contactForm">
<input name="id" type="hidden" value="136" />
<input name="email" type="email"/>
<input name="name" type="string" />
<input name="subscriber" type="checkbox" />
</form>
<script>
updateContact(Object.fromEntries(
new FormData(document.forms.contactForm));
</script>
Copy the code
Using hidden input and a useful FormData class, we can seamlessly convert values between DOM input and JavaScript functions.
Combined form and reactive
By combining the form’s high-performance selector stability with CSS responsiveness, we can achieve more complex UI logic:
<form name="contactForm">
<input name="showErrors" type="checkbox" hidden />
<fieldset name="names">
<input name="name" />
<output name="error"></output>
</fieldset>
<fieldset name="emails">
<input name="email" />
<output name="error"></output>
</fieldset>
</form>
<script>
function setErrorMessage(section, message) {
document.forms.contactForm.elements[section].elements.error.value = message;
}
function setShowErrors(show) {
document.forms.contactForm.elements.showErrors.checked = show;
}
</script>
<style>
input[name="showErrors"]:not(:checked) ~ * output[name="error"] {
display: none;
}
</style>
Copy the code
Note that classes are not used in this example — we develop the behavior and style of the DOM from the form’s data, rather than manually changing the element classes.
I don’t like to overuse CSS classes as JavaScript selectors. I think they should be used to group similarly styled elements together, rather than as a one-size-fits-all mechanism for changing component styles.
Advantages of forms
- Like cascades, forms are built on a Web platform, and most of their features are stable. This means less JavaScript, less framework version mismatches, and no “builds”.
- By default, forms are accessible. If your application uses forms correctly, there will be much less need for ARIA properties, “accessibility plug-ins,” and finally auditing. The form itself can be used for keyboard navigation, screen readers, and other assistive technologies.
- Forms have built-in input validation: validation through regular expressions, reaction to invalid and valid forms in CSS, processing required and optional forms, and more. You don’t need something that looks like a form to enjoy these features.
- Form submission events are very useful. For example, it allows the “Enter” key to be captured without a submit button, and allows multiple submit buttons to be distinguished by submitter attributes (as we’ll see later in the TODO example).
- By default, elements are associated with the form they contain, but you can use the form attribute to associate with any other form in the document. This allows us to handle form associations without relying on the DOM tree.
- Using stable selectors helps with UI test automation: we can use nested apis as a stable way to hook into the DOM, regardless of its layout and hierarchy. The Form > FieldSet > Element hierarchy can be used as an interaction framework for documents.
Chacha and HTML templates
The framework provides its own way of expressing a watchlist. Today, many developers also rely on non-framework libraries that provide such features, such as MobX.
The main problem with generic purpose watchlists is that they are generic. This increases convenience while reducing performance, and requires special development tools to debug the complex operations these libraries perform in the background.
It’s ok to use these libraries and understand what they do, and they’re useful regardless of your choice of UI framework, but using an alternative approach might not be more complicated, and it might prevent some of the pitfalls that occur when you try to run your own model.
Change channel (or CHACHA)
A ChaCha — also known as a change channel — is a two-way flow whose purpose is to notify changes in the direction of intent and observation.
- In the intent direction, the UI notifies the model of the changes the user wants to make.
- In the view direction, the model notifies the UI of changes made to the model and changes that need to be displayed to the user.
It may be an interesting name, but it’s not a complicated or novel model. Two-way flows are ubiquitous in networks and software (MessagePort, for example). In this case, we created a two-way flow that has a special purpose: reporting actual model changes to the UI and reporting intents to the model.
ChaCha’s interface can often be derived from the application’s specification without any UI code.
For example, an application that allows you to add and remove contacts and load the initial list from the server (with refresh options) could have a ChaCha like this:
interface Contact {
id: string;
name: string;
email: string;
}
// "Observe" Direction
interface ContactListModelObserver {
onAdd(contact: Contact);
onRemove(contact: Contact);
onUpdate(contact: Contact);
}
// "Intent" Direction
interface ContactListModel {
add(contact: Contact);
remove(contact: Contact);
reloadFromServer();
}
Copy the code
Note that all functions in both interfaces are void and only accept normal objects. This is intentional. ChaCha is built like a two-port channel to send messages, which allows it to work in EventSource, HTML MessageChannel, service Worker, or any other protocol.
The advantage of ChaChas is that it is easy to test: you send the action and expect that particular call to be returned to the observer.
The HTML template element for the list item
HTML templates are special elements that exist in the DOM but are not displayed. Their purpose is to generate dynamic elements.
When we use template elements, we can avoid all the boilerplate code that creates elements and populates them in JavaScript.
The following will use the template to add a name to the list:
<ul id="names">
<template>
<li><label class="name" /></li>
</template>
</ul>
<script>
function addName(name) {
const list = document.querySelector('#names');
const item = list.querySelector('template').content.cloneNode(true).firstElementChild;
item.querySelector('label').innerText = name;
list.appendChild(item);
}
</script>
Copy the code
By using the template element for the list item, we can see the list item in the raw HTML — it is not “rendered” in JSX or any other language. Your HTML file now contains all of your application’s HTML-static parts are part of the render DOM, and the dynamic parts are represented in the template, ready to be cloned and appended to the document when the time is right.
Put it all together :TodoMVC
TodoMVC is an application specification for TODO lists that show different frameworks. The TodoMVC template comes with ready-made HTML and CSS to help you focus on the framework.
You can use the results in the GitHub library, where the full source code is available.
Chacha derived from the specification
We’ll start with the specification and use it to build the ChaCha interface:
interface Task {
title: string;
completed: boolean;
}
interface TaskModelObserver {
onAdd(key: number, value: Task);
onUpdate(key: number, value: Task);
onRemove(key: number);
onCountChange(count: {active: number, completed: number});
}
interface TaskModel {
constructor(observer: TaskModelObserver);
createTask(task: Task): void;
updateTask(key: number, task: Task): void;
deleteTask(key: number): void;
clearCompleted(): void;
markAll(completed: boolean): void;
}
Copy the code
The functionality in the task model is derived directly from the specification and what users can do (clear completed tasks, mark all tasks as completed or active, and get counts of activities and completed).
Note that it follows ChaCha’s guiding principles:
- There are two interfaces, one is the action interface, one is the observation interface.
- All parameter types are primitives or plain objects (easily converted to JSON).
- All functions return void.
- The TodoMVC implementation uses localStorage as the back end.
The model is simple enough that it doesn’t have much to do with the DISCUSSION of UI frameworks. It saves to localStorage when needed and issues change callbacks to observers when some change occurs, either as a result of user action or when the model is first loaded from localStorage.
Lean, forms-oriented HTML
Next, I’ll use the TodoMVC template and modify it to be form-oriented — a form hierarchy, with input and output elements representing data that can be changed with JavaScript.
How do I know if I need a form element? As a rule of thumb, if it is bound to data in the model, it should be a form element.
The full HTML code is available, but here’s the main part of it:
<section class="todoapp">
<header class="header">
<h1>todos</h1>
<form name="newTask">
<input name="title" type="text" placeholder="What needs to be done?" autofocus>
</form>
</header>
<main>
<form id="main"></form>
<input type="hidden" name="filter" form="main" />
<input type="hidden" name="completedCount" form="main" />
<input type="hidden" name="totalCount" form="main" />
<input name="toggleAll" type="checkbox" form="main" />
<ul class="todo-list">
<template>
<form class="task">
<li>
<input name="completed" type="checkbox" checked>
<input name="title" readonly />
<input type="submit" hidden name="save" />
<button name="destroy">X</button>
</li>
</form>
</template>
</ul>
</main>
<footer>
<output form="main" name="activeCount">0</output>
<nav>
<a name="/" href="#/">All</a>
<a name="/active" href="#/active">Active</a>
<a name="/completed" href="#/completed">Completed</a>
</nav>
<input form="main" type="button" name="clearCompleted" value="Clear completed" />
</footer>
</section>
Copy the code
This HTML includes the following:
- We have a main form that contains all global inputs and buttons, and a new form for creating new tasks. Notice that we use the form attribute to associate elements with the form to avoid elements being nested within the form.
- The template element represents a list item whose root element is another form that represents interactive data related to a particular task. When adding tasks, you can repeat this form by cloning the contents of the template.
- Hidden input represents data that is not directly displayed, but is used for styling and selection.
Notice how succinct the DOM is. It has no discrete classes in its elements. It contains all the elements your application needs, arranged in a reasonable hierarchy. Thanks to the hidden input elements, you already have a good idea of how the document might change later.
The HTML doesn’t know how it will be styled or what data it will bind to. Make CSS and JavaScript work for HTML, not HTML work for a specific style mechanism. This will make it easier to change the design.
Minimal Controller– javascript
Now that we have most of the reactivity in the CSS, and list processing in the model, all that’s left is the Controller code — the duct tape that holds everything together. In this small application, the Controller JavaScript has about 40 lines of code.
Here’s a version, with an explanation of each part:
import TaskListModel from './model.js';
const model = new TaskListModel(new class {
Copy the code
In the code above, we created a new model.
onAdd(key, value) {
const newItem = document.querySelector('.todo-list template').content.cloneNode(true).firstElementChild;
newItem.name = `task-${key}`;
const save = () => model.updateTask(key, Object.fromEntries(new FormData(newItem)));
newItem.elements.completed.addEventListener('change', save);
newItem.addEventListener('submit', save);
newItem.elements.title.addEventListener('dblclick', ({target}) => target.removeAttribute('readonly'));
newItem.elements.title.addEventListener('blur', ({target}) => target.setAttribute('readonly', ''));
newItem.elements.destroy.addEventListener('click', () => model.deleteTask(key));
this.onUpdate(key, value, newItem);
document.querySelector('.todo-list').appendChild(newItem);
}
Copy the code
When an item is added to the model, we create the corresponding list item in the UI.
Above, we cloned the contents of the item template, assigned event listeners to specific items, and added new items to the list.
Notice that this function, along with onUpdate, onRemove, and onCountChange, is a callback function called from the model.
onUpdate(key, {title, completed}, form = document.forms[`task-${key}`]) { form.elements.completed.checked = !! completed; form.elements.title.value = title; form.elements.title.blur(); }Copy the code
When a project is updated, we set its complete and title values and lose focus (exit edit mode).
onRemove(key) { document.forms[`task-${key}`].remove(); }
Copy the code
When an item is removed from the model, we remove its corresponding list item from the view.
onCountChange({active, completed}) {
document.forms.main.elements.completedCount.value = completed;
document.forms.main.elements.toggleAll.checked = active === 0;
document.forms.main.elements.totalCount.value = active + completed;
document.forms.main.elements.activeCount.innerHTML = `<strong>${active}</strong> item${active === 1 ? '' : 's'} left`;
}
Copy the code
In the code above, when the number of completed or active items changes, we set the appropriate input to trigger a CSS response and format the output that displays the count.
const updateFilter = () => filter.value = location.hash.substr(2);
window.addEventListener('hashchange', updateFilter);
window.addEventListener('load', updateFilter);
Copy the code
We then update the filter from the hash fragment (and at startup). All we did above was set the value of a form element — CSS takes care of the rest.
document.querySelector('.todoapp').addEventListener('submit', e => e.preventDefault(), {capture: true});
Copy the code
Here, we make sure that the page does not reload when the form is submitted. That’s the line that turned this app into a SPA.
document.forms.newTask.addEventListener('submit', ({target: {elements: {title}}}) =>
model.createTask({title: title.value}));
document.forms.main.elements.toggleAll.addEventListener('change', ({target: {checked}})=>
model.markAll(checked));
document.forms.main.elements.clearCompleted.addEventListener('click', () =>
model.clearCompleted());
Copy the code
This handles the main operations (create, mark all, clean up).
Use CSS for responsiveness
You can view the complete CSS code.
CSS addresses many of the requirements in the specification (with some modifications for ease of access). Let’s look at some examples.
According to the specification, the “X” (destroy) button is only displayed when hovering. I also added an accessibility bit to make it visible in the task set:
.task:not(:hover, :focus-within) button[name="destroy"] { opacity: 0 }
Copy the code
When the filter link is the current link, it gets a red border:
.todoapp input[name="filter"][value=""] ~ footer a[href$="#/"],
nav a:target {
border-color: #CE4646;
}
Copy the code
Note that we can use the href of the link element as a partial property selector — no JavaScript is required to check the current filter and set a selected class on the appropriate element.
We also use the: Target selector, so we don’t have to worry about adding filters.
The view and edit style of the title input changes according to its read-only mode:
. Task input [name = "title"] : the read - only {... }.task Input [name="title"]:not(:read-only) {... }Copy the code
Filtering (that is, showing only active and completed tasks) is done with selectors:
input[name="filter"][value="active"] ~ * .task
:is(input[name="completed"]:checked, input[name="completed"]:checked ~ *),
input[name="filter"][value="completed"] ~ * .task
:is(input[name="completed"]:not(:checked), input[name="completed"]:not(:checked) ~ *) {
display: none;
}
Copy the code
The above code may seem a bit wordy and may be easier to read using CSS preprocessors such as Sass. But what it does is simple: if the filter is active and the completed checkbox is selected, or vice versa, then we hide the checkbox and its sibling elements.
I chose to implement this simple filter in CSS to show how far it can go, but if it starts to get complicated then it will make perfect sense to move into the model.
Conclusions and Main points
I believe that frameworks provide a convenient way to implement complex tasks, in addition to the technical benefits, such as having a group of developers follow a particular style and pattern. The Web platform offers many choices, and using a framework allows everyone to be at least partially on the same page on some choices. That’s valuable. There is also something to be said for the elegance of declarative programming, and the main features of componentization are not covered in this article.
Keep in mind, however, that alternative models exist, usually at a lower cost, and don’t always require less developer experience. Allow yourself to be curious about these patterns, even if you decide to pick and choose between them when using the framework.
Model review
- Keep the DOM tree stable. It starts a chain reaction that makes things easier.
- Where possible, rely on CSS rather than JavaScript to implement responsiveness.
- Use form elements as the primary means of presenting interactive data.
- Use HTML template elements instead of javascript-generated templates.
- Use a two-way data flow as the interface to the model.
Thank you again for your work :Yehonatan Daniv, Tom Bigelajzen, Benjamin Greenbaum, Nick Ribal, Louis Lazaris.