1677556374
Signals are the new hotness on Tech Twitter, but are they for real or just hype. Let's find out as we figure out what signals are, and how to us them in Solid, Qwik and React.
0:00 Introduction
1:31 Example Application
2:13 Solid Implementation
7:46 Computed State
10:16 Qwik Implementation
14:28 Qwik take on Qwik
15:04 What is Fine Grained Updating?
17:52 Hacking "Fine Grained" Into React
20:47 Preact Signals For React
24:30 Jotai Signals
27:34 Outroduction
Code: https://github.com/jherr/signals-video-code
#react #solid #qwik #signals
1676623546
In this tutorial, I’ll show you what the SOLID principles entail, what each part of the SOLID acronym means, and how to implement them in your code. What are SOLID Design Principles? SOLID is a set of five design principles: The Single Responsibility Principle (SRP), The Open-Closed Principle (OCP), The Liskov Substitution Principle (LSP), The Interface Segregation Principle (ISP), The Dependency Inversion Principle (DIP).
SOLID is a set of five design principles. These principles help software developers design robust, testable, extensible, and maintainable object-oriented software systems.
Each of these five design principles solves a particular problem that might arise while developing the software systems.
In this article, I’ll show you what the SOLID principles entail, what each part of the SOLID acronym means, and how to implement them in your code.
SOLID is an acronym that stands for:
In the coming sections, we’ll look at what each of those principles means in detail.
The SOLID design principle is a subcategory of many principles introduced by the American scientist and instructor, Robert C. Martin (A.K.A Uncle Bob) in a 2000 paper.
Following these principles can result in a very large codebase for a software system. But in the long run, the main aim of the principles is never defeated. That is, helping software developers make changes to their code without causing any major issues.
The single responsibility principle states that a class, module, or function should have only one reason to change, meaning it should do one thing.
For example, a class that shows the name of an animal should not be the same class that displays the kind of sound it makes and how it feeds.
Here’s an example in JavaScript:
class Animal {
constructor(name, feedingType, soundMade) {
this.name = name;
this.feedingType = feedingType;
this.soundMade = soundMade;
}
nomenclature() {
console.log(`The name of the animal is ${this.name}`);
}
sound() {
console.log(`${this.name} ${this.soundMade}s`);
}
feeding() {
console.log(`${this.name} is a ${this.feedingType}`);
}
}
let elephant = new Animal('Elephant', 'herbivore', 'trumpet');
elephant.nomenclature(); // The name of the animal is Elephant
elephant.sound(); // Elephant trumpets
elephant.feeding(); // Elephant is a herbivore
The code above violates the single responsibility principle because the class that's responsible for printing the name of the animal also shows the sound it makes and its type of feeding.
To fix this, you have to create a separate class for the sound and feeding methods like this:
class Animal {
constructor(name) {
this.name = name;
}
nomenclature() {
console.log(`The name of the animal is ${this.name}`);
}
}
let animal1 = new Animal('Elephant');
animal1.nomenclature(); // The name of the animal is Elephant
// Sound class
class Sound {
constructor(name, soundMade) {
this.name = name;
this.soundMade = soundMade;
}
sound() {
console.log(`${this.name} ${this.soundMade}s`);
}
}
let animalSound1 = new Sound('Elephant', 'trumpet');
animalSound1.sound(); //Elephant trumpets
// Feeding class
class Feeding {
constructor(name, feedingType) {
this.name = name;
this.feedingType = feedingType;
}
feeding() {
console.log(`${this.name} is a/an ${this.feedingType}`);
}
}
let animalFeeding1 = new Feeding('Elephant', 'herbivore');
animalFeeding1.feeding(); // Elephant is a/an herbivore
This way, each of the classes is doing only one thing:
That’s more code, but better readability and maintainability. A developer who didn’t write the code can come to it and understand what’s going on quicker than having it all in one class.
The open-closed principle states that classes, modules, and functions should be open for extension but closed for modification.
This principle might seem to contradict itself, but you can still make sense of it in code. It means you should be able to extend the functionality of a class, module, or function by adding more code without modifying the existing code.
Here’s some code that violates the open-closed principle in JavaScript:
class Animal {
constructor(name, age, type) {
this.name = name;
this.age = age;
this.type = type;
}
getSpeed() {
switch (this.type) {
case 'cheetah':
console.log('Cheetah runs up to 130mph ');
break;
case 'lion':
console.log('Lion runs up to 80mph');
break;
case 'elephant':
console.log('Elephant runs up to 40mph');
break;
default:
throw new Error(`Unsupported animal type: ${this.type}`);
}
}
}
const animal1 = new Animal('Lion', 4, 'lion');
animal1.getSpeed(); // Lion runs up to 80mph
The code above violates the open-closed principle because if you want to add a new animal type, you have to modify the existing code by adding another case to the switch statement.
Normally, if you’re using a switch
statement, then it’s very likely you will violate the open-closed principle.
Here’s how I refactored the code to fix the problem:
class Animal {
constructor(name, age, speedRate) {
this.name = name;
this.age = age;
this.speedRate = speedRate;
}
getSpeed() {
return this.speedRate.getSpeed();
}
}
class SpeedRate {
getSpeed() {}
}
class CheetahSpeedRate extends SpeedRate {
getSpeed() {
return 130;
}
}
class LionSpeedRate extends SpeedRate {
getSpeed() {
return 80;
}
}
class ElephantSpeedRate extends SpeedRate {
getSpeed() {
return 40;
}
}
const cheetah = new Animal('Cheetah', 4, new CheetahSpeedRate());
console.log(`${cheetah.name} runs up to ${cheetah.getSpeed()} mph`); // Cheetah runs up to 130 mph
const lion = new Animal('Lion', 5, new LionSpeedRate());
console.log(`${lion.name} runs up to ${lion.getSpeed()} mph`); // Lion runs up to 80 mph
const elephant = new Animal('Elephant', 10, new ElephantSpeedRate());
console.log(`${elephant.name} runs up to ${elephant.getSpeed()} mph`); // Elephant runs up to 40 mph
This way, if you want to add a new animal type, you can create a new class that extends SpeedRate
and pass it to the Animal constructor without modifying the existing code.
For example, I added a new GoatSpeedRate
class like this:
class GoatSpeedRate extends SpeedRate {
getSpeed() {
return 35;
}
}
// Goat
const goat = new Animal('Goat', 5, new GoatSpeedRate());
console.log(`${goat.name} runs up to ${goat.getSpeed()} mph`); // Goat runs up to 354 mph
This conforms to the open-closed principle.
The Liskov substitution principle is one of the most important principles to adhere to in object-oriented programming (OOP). It was introduced by the computer scientist Barbara Liskov in 1987 in a paper she co-authored with Jeannette Wing.
The principle states that child classes or subclasses must be substitutable for their parent classes or super classes. In other words, the child class must be able to replace the parent class. This has the advantage of letting you know what to expect from your code.
Here’s an example of a code that does not violate the Liskov substitution principle:
class Animal {
constructor(name) {
this.name = name;
}
makeSound() {
console.log(`${this.name} makes a sound`);
}
}
class Dog extends Animal {
makeSound() {
console.log(`${this.name} barks`);
}
}
class Cat extends Animal {
makeSound() {
console.log(`${this.name} meows`);
}
}
function makeAnimalSound(animal) {
animal.makeSound();
}
const cheetah = new Animal('Cheetah');
makeAnimalSound(cheetah); // Cheetah makes a sound
const dog = new Dog('Jack');
makeAnimalSound(dog); // Jack barks
const cat = new Cat('Khloe');
makeAnimalSound(cat); // Khloe meows
The Dog
and Cat
classes can successfully replace the parent Animal
class.
On the other hand, let’s look at how the code below does violate the Liskov substitution principle:
class Bird extends Animal {
fly() {
console.log(`${this.name} flaps wings`);
}
}
const parrot = new Bird('Titi the Parrot');
makeAnimalSound(parrot); // Titi the Parrot makes a sound
parrot.fly(); // Titi the Parrot flaps wings
The Bird
class violates the Liskov substitution principle because it’s not implementing its own makeSound
from the parent Animal
class. Instead, it’s inheriting the generic sound.
To fix this, you have to make it use the makeSound
method too:
class Bird extends Animal {
makeSound() {
console.log(`${this.name} chirps`);
}
fly() {
console.log(`${this.name} flaps wings`);
}
}
const parrot = new Bird('Titi the Parrot');
makeAnimalSound(parrot); // Titi the Parrot chirps
parrot.fly(); // Titi the Parrot flaps wings
The interface segregation principle states that clients should not be forced to implement interfaces or methods they do not use.
More specifically, the ISP suggests that software developers should break down large interfaces into smaller, more specific ones, so that clients only need to depend on the interfaces that are relevant to them. This can make the codebase easier to maintain.
This principle is fairly similar to the single responsibility principle (SRP). But it’s not just about a single interface doing only one thing – it’s about breaking the whole codebase into multiple interfaces or components.
Think about this as the same thing you do while working with frontend frameworks and libraries like React, Svelte, and Vue. You usually break down the codebase into components you only bring in when needed.
This means you create individual components that have functionality specific to them. The component responsible for implementing scroll to the top, for example, will not be the one to switch between light and dark, and so on.
Here’s an example of code that violates the interface segregation principle:
class Animal {
constructor(name) {
this.name = name;
}
eat() {
console.log(`${this.name} is eating`);
}
swim() {
console.log(`${this.name} is swimming`);
}
fly() {
console.log(`${this.name} is flying`);
}
}
class Fish extends Animal {
fly() {
console.error("ERROR! Fishes can't fly");
}
}
class Bird extends Animal {
swim() {
console.error("ERROR! Birds can't swim");
}
}
const bird = new Bird('Titi the Parrot');
bird.swim(); // ERROR! Birds can't swim
const fish = new Fish('Neo the Dolphin');
fish.fly(); // ERROR! Fishes can't fly
The code above violates the interface segregation principle because the Fish
class doesn’t need the fly
method. A fish cannot fly. Birds can’t swim too, so the Bird
class doesn’t need the swim
method.
This is how I fixed the code to conform to the interface segregation principle:
class Animal {
constructor(name) {
this.name = name;
}
eat() {
console.log(`${this.name} is eating`);
}
swim() {
console.log(`${this.name} is swimming`);
}
fly() {
console.log(`${this.name} is flying`);
}
}
class Fish extends Animal {
// This class needs the swim() method
}
class Bird extends Animal {
// THis class needs the fly() method
}
// Making them implement the methods they need
const bird = new Bird('Titi the Parrot');
bird.swim(); // Titi the Parrot is swimming
const fish = new Fish('Neo the Dolphin');
fish.fly(); // Neo the Dolphin is flying
console.log('\n');
// Both can also implement eat() method of the Super class because they both eat
bird.eat(); // Titi the Parrot is eating
fish.eat(); // Neo the Dolphin is eating
The dependency inversion principle is about decoupling software modules. That is, making them as separate from one another as possible.
The principle states that high-level modules should not depend on low-level modules. Instead, they should both depend on abstractions. Additionally, abstractions should not depend on details, but details should depend on abstractions.
In simpler terms, this means instead of writing code that relies on specific details of how lower-level code works, you should write code that depends on more general abstractions that can be implemented in different ways.
This makes it easier to change the lower-level code without having to change the higher-level code.
Here’s a code that violates the dependency inversion principle:
class Animal {
constructor(name) {
this.name = name;
}
}
class Dog extends Animal {
bark() {
console.log('woof! woof!! woof!!');
}
}
class Cat extends Animal {
meow() {
console.log('meooow!');
}
}
function printAnimalNames(animals) {
for (let i = 0; i < animals.length; i++) {
const animal = animals[i];
console.log(animal.name);
}
}
const dog = new Dog('Jack');
const cat = new Cat('Zoey');
const animals = [dog, cat];
printAnimalNames(animals);
The code above violates dependency inversion principle because the printAnimalNames
function depends on the concrete implementations of Dog and Cat.
If you wanted to add another animal like an ape, you’d have to modify the printAnimalNames
function to handle it.
Here’s how to fix it:
class Animal {
constructor(name) {
this.name = name;
}
getName() {
return this.name;
}
}
class Dog extends Animal {
bark() {
console.log('woof! woof!! woof!!!');
}
}
class Cat extends Animal {
meow() {
console.log('meooow!');
}
}
function printAnimalNames(animals) {
for (let i = 0; i < animals.length; i++) {
const animal = animals[i];
console.log(animal.getName());
}
}
const dog = new Dog('Jack');
const cat = new Cat('Zoey');
const animals = [dog, cat, ape];
printAnimalNames(animals);
In the code above, I created a getName
method inside the Animal class. This provides an abstraction that the printAnimalNames
function can depend on. Now, the printAnimalNames
function only depends on the Animal
class, not the concrete implementations of Dog
and Cat
.
If you wan to add an Ape class, you can do so without modifying the printAnimalNames
function:
class Animal {
constructor(name) {
this.name = name;
}
getName() {
return this.name;
}
}
class Dog extends Animal {
bark() {
console.log('woof! woof!! woof!!!');
}
}
class Cat extends Animal {
meow() {
console.log('meooow!');
}
}
// Add Ape class
class Ape extends Animal {
meow() {
console.log('woo! woo! woo!');
}
}
function printAnimalNames(animals) {
for (let i = 0; i < animals.length; i++) {
const animal = animals[i];
console.log(animal.getName());
}
}
const dog = new Dog('Jack'); // Jack
const cat = new Cat('Zoey'); // Zoey
// Use the Ape class
const ape = new Ape('King Kong'); // King Kong
const animals = [dog, cat, ape];
printAnimalNames(animals);
In this article, you learned what the SOLID design priniples are. We discussed each principle with an example, and went through ways to implement them in JavaScript.
I hope the article gives you a solid grasp of the SOLID principles. You can see that the SOLID design principles can help you create a software system that is free of bugs, maintainable, flexible, scalable, and reusable.
Thanks for reading.
Original article source at https://www.freecodecamp.org
#solid
1675516620
Android Components Architecture in a Modular Word is a sample project that presents modern, 2020 approach to Android application development using Kotlin and latest tech-stack.
The goal of the project is to demonstrate best practices, provide a set of guidelines, and present modern Android application architecture that is modular, scalable, maintainable and testable. This application may look simple, but it has all of these small details that will set the rock-solid foundation of the larger app suitable for bigger teams and long application lifecycle management.
The project received different mentions/reference from Android Developer Community:
First off, you require the latest Android Studio 4.1.0 (or newer) to be able to build the app.
You need to supply keys for Marvel API. You can find information about how to gain access by using the link.
When you obtain the keys, you can provide them to the app by putting the following in the local.properties
project root file:
#Marvel API KEYS
marvel.key.public = <insert>
marvel.key.private = <insert>
To maintain the style and quality of the code, are used the bellow static analysis tools. All of them use properly configuration and you find them in the project root directory .{toolName}
.
Tools | Config file | Check command | Fix command |
---|---|---|---|
detekt | /.detekt | ./gradlew detekt | - |
ktlint | - | ./gradlew ktlint | ./gradlew ktlintFormat |
spotless | /.spotless | ./gradlew spotlessCheck | ./gradlew spotlessApply |
lint | /.lint | ./gradlew lint | - |
All these tools are integrated in pre-commit git hook, in order ensure that all static analysis and tests passes before you can commit your changes. To skip them for specific commit add this option at your git command:
git commit --no-verify
The pre-commit git hooks have exactly the same checks as CircleCI and are defined in this script. This step ensures that all commits comply with the established rules. However the continuous integration will ultimately be validated that the changes are correct.
App support different screen sizes and the content has been adapted to fit for mobile devices and tablets. To do that, it has been created a flexible layout using one or more of the following concepts:
In terms of design has been followed recommendations android material design comprehensive guide for visual, motion, and interaction design across platforms and devices. Granting the project in this way a great user experience (UX) and user interface (UI). For more info about UX best practices visit link.
Moreover, has been implemented support for dark theme with the following benefits:
The architecture of the application is based, apply and strictly complies with each of the following 5 points:
Modules are collection of source files and build settings that allow you to divide a project into discrete units of functionality. In this case apart from dividing by functionality/responsibility, existing the following dependence between them:
The above graph shows the app modularisation:
:app
depends on :core
and indirectly depends on :features
by dynamic-features.:features
modules depends on :commons
, :core
, :libraries
and :app
.:core
and :commons
only depends for possible utils on :libraries
.:libraries
don’t have any dependency.The :app
module is an com.android.application, which is needed to create the app bundle. It is also responsible for initiating the dependency graph, play core and another project global libraries, differentiating especially between different app environments.
The :core
module is an com.android.library for serving network requests or accessing to the database. Providing the data source for the many features that require it.
The :features
module are an com.android.dynamic-feature is essentially a gradle module which can be downloaded independently from the base application module. It can hold code and resources and include dependencies, just like any other gradle module.
The :commons
modules are an com.android.library only contains code and resources which are shared between feature modules. Reusing this way resources, layouts, views, and components in the different features modules, without the need to duplicate code.
The :libraries
modules are an com.android.library, basically contains different utilities that can be used by the different modules.
Ideally, ViewModels shouldn’t know anything about Android. This improves testability, leak safety and modularity. ViewModels have different scopes than activities or fragments. While a ViewModel is alive and running, an activity can be in any of its lifecycle states. Activities and fragments can be destroyed and created again while the ViewModel is unaware.
Passing a reference of the View (activity or fragment) to the ViewModel is a serious risk. Lets assume the ViewModel requests data from the network and the data comes back some time later. At that moment, the View reference might be destroyed or might be an old activity that is no longer visible, generating a memory leak and, possibly, a crash.
The communication between the different layers follow the above diagram using the reactive paradigm, observing changes on components without need of callbacks avoiding leaks and edge cases related with them.
The application has different product flavours: Dev
, QA
, Prod
. Each variant has a specific target environment and to make easier to distinguish them the app uses a specific icon colour for debug
and release
build variant with descriptive app name. In this case and given that it's a sample, all variants have the same Marvel API endpoint. But the idea is to have different environments target for Development and QA respectively, what doesn't affect the production environment. This is applicable to any tool, platform, service what is being used. For more information about build variant, check this link.
The documentation is generated following KDoc language (the equivalent of Java's JavaDoc) via documentation engine for Kotlin Dokka.
To consult it check this link or open the project /docs
directory.
This project takes advantage of many popular libraries, plugins and tools of the Android ecosystem. Most of the libraries are in the stable version, unless there is a good reason to use non-stable dependency.
This is project is a sample, to inspire you and should handle most of the common cases, but obviously not all. If you need to take a look at additional resources to find solutions for your project, visit these interesting projects:
A collection of very interesting articles related last android community tendencies and recommendations for start to take in consideration for your current/next project:
The open-source community create and maintains tons of awesome libraries making your job more easy, giving the opportunity to use them in your developments. Here are a very important collection of them:
Avoid reinventing the wheel by following these guidelines:
Google Developers Codelabs provide a guided, tutorial, hands-on coding experience. Most codelabs will step you through the process of building a small application, or adding a new feature to an existing application. They cover a wide range of android concepts to learn and practice:
All contributions are welcome! Please feel free to post questions, recommendations, ideas, bugs by create new issue following the template or if you want create directly new pull request.
Author: vmadalin
Source Code: https://github.com/vmadalin/android-modular-architecture
License: Apache-2.0 license
1672988400
This project is an example of architecture using new technologies and best practices.
The goal is to learn and share knowledge and use it as reference for new projects.
Command Line
Visual Studio Code
Visual Studio
Docker
Source: https://github.com/rafaelfgx/DotNetCore
Published: https://www.nuget.org/profiles/rafaelfgx
Web: Frontend and Backend.
Application: Flow control.
Domain: Business rules and domain logic.
Model: Data transfer objects.
Database: Data persistence.
It is the interface between frontend and backend and has logic that does not belong in components.
export class AppCustomerService { }
It validates if a route can be activated.
export class AppGuard implements CanActivate { }
It provides a hook for centralized exception handling.
export class AppErrorHandler implements ErrorHandler { }
It intercepts and handles an HttpRequest or HttpResponse.
export class AppHttpInterceptor implements HttpInterceptor { }
It has no any logic, business rules or dependencies other than mediator.
[ApiController]
[Route("customers")]
public sealed class CustomerController : BaseController
{
[HttpPost]
public IActionResult Add(AddCustomerRequest request) => Mediator.HandleAsync<AddCustomerRequest, AddCustomerResponse>(request).ApiResult();
[HttpDelete("{id:long}")]
public IActionResult Delete(long id) => Mediator.HandleAsync(new DeleteCustomerRequest(id)).ApiResult();
[HttpGet("{id:long}")]
public IActionResult Get(long id) => Mediator.HandleAsync<GetCustomerRequest, GetCustomerResponse>(new GetCustomerRequest(id)).ApiResult();
[HttpGet("grid")]
public IActionResult Grid([FromQuery] GridCustomerRequest request) => Mediator.HandleAsync<GridCustomerRequest, GridCustomerResponse>(request).ApiResult();
[HttpGet]
public IActionResult List() => Mediator.HandleAsync<ListCustomerRequest, ListCustomerResponse>(new ListCustomerRequest()).ApiResult();
[HttpPut("{id:long}")]
public IActionResult Update(UpdateCustomerRequest request) => Mediator.HandleAsync(request).ApiResult();
}
It has only business flow, not business rules.
It has properties representing the request.
public sealed record AddCustomerRequest;
It has rules for validating the request.
public sealed class AddCustomerRequestValidator : AbstractValidator<AddCustomerRequest> { }
It has properties representing the response.
public sealed record AddCustomerResponse;
It is responsible for the business flow and processing a request to return a response.
It call factories, repositories, unit of work, services or mediator, but it has no business rules.
public sealed record AddCustomerHandler : IHandler<AddCustomerRequest, AddCustomerResponse>
{
public async Task<Result<AddCustomerResponse>> HandleAsync(AddCustomerRequest request)
{
return Result<AddCustomerResponse>.Success(new AddCustomerResponse());
}
}
It creates a complex object.
Any change to object affects compile time rather than runtime.
public interface ICustomerFactory { }
public sealed class CustomerFactory : ICustomerFactory { }
It has no any references to any layer.
It has aggregates, entities, value objects and services.
It defines a consistency boundary around one or more entities.
The purpose is to model transactional invariants.
One entity in an aggregate is the root, any other entities in the aggregate are children of the root.
public sealed class CustomerAggregate { }
It has unique identity. Identity may span multiple bounded contexts and may endure beyond the lifetime.
Changing properties is only allowed through internal business methods in the entity, not through direct access to the properties.
public sealed class Customer : Entity { }
It has no identity and it is immutable.
It is defined only by the values of its properties.
To update a value object, you must create a new instance to replace the old one.
It can have methods that encapsulate domain logic, but these methods must have no side effects on the state.
public sealed record ValueObject;
It performs domain operations and business rules.
It is stateless and has no operations that are not a part of an entity or value object.
public interface ICustomerService { }
public sealed class CustomerService : ICustomerService { }
It has properties to transport and return data.
public sealed record CustomerModel;
It encapsulates data persistence.
It configures the connection and represents the database.
public sealed class Context : DbContext
{
public Context(DbContextOptions options) : base(options) { }
}
It configures the entity and its properties in the database.
public sealed class CustomerConfiguration : IEntityTypeConfiguration<Customer>
{
public void Configure(EntityTypeBuilder<Customer> builder) { }
}
It inherits from the generic repository and only implements specific methods.
public interface ICustomerRepository : IRepository<Customer> { }
public sealed class CustomerRepository : Repository<Customer>, ICustomerRepository
{
public CustomerRepository(Context context) : base(context) { }
}
Author: rafaelfgx
Source Code: https://github.com/rafaelfgx/Architecture
License: MIT license
1668414766
SOLID is that stands for the five design principles of Object-Oriented class design. In this tutorial, you'll learn what these principles stand for and how they work using JavaScript examples.
Robert C. Martin – also known as Uncle Bob – is a well-known American software engineer. He's the author of the bestselling books Clean Code and Clean Architecture.
In 2000, he wrote a paper titled Design Principles and Design Patterns where he introduced some principles to help developers write good code in object-oriented programming.
These principles are meant to make object-oriented designs more understandable, flexible, and maintainable. They make it easy to create readable and testable code that many developers can collaboratively work with anywhere and anytime. And they make you aware of the right way to write code 💪.
All the principles in the paper together form the SOLID acronym, which was later introduced by Michael Feathers.
SOLID is a mnemonic acronym that stands for the five design principles of Object-Oriented class design. These principles are:
In this article, you will learn what these principles stand for and how they work using JavaScript examples. The examples should be fine even if you are not fully conversant with JavaScript, because they apply to other programming languages as well.
The Single-responsibility Principle, or SRP, states that a class should only have one reason to change. This means that a class should have only one job and do one thing.
Let’s take a look at a proper example. You’ll always be tempted to put similar classes together – but unfortunately, this goes against the Single-responsibility principle. Why?
The ValidatePerson
object below has three methods: two validation methods, (ValidateName()
and ValidateAge()
), and a Display()
method.
class ValidatePerson {
constructor(name, age) {
this.name = name;
this.age = age;
}
ValidateName(name) {
if (name.length > 3) {
return true;
} else {
return false;
}
}
ValidateAge(age) {
if (age > 18) {
return true;
} else {
return false;
}
}
Display() {
if (this.ValidateName(this.name) && this.ValidateAge(this.age)) {
console.log(`Name: ${this.name} and Age: ${this.age}`);
} else {
console.log('Invalid');
}
}
}
The Display()
method goes against the SRP because the goal is that a class should have only one job and do one thing. The ValidatePerson
class does two jobs – it validates the person’s name and age and then displays some information.
The way to avoid this problem is to separate code that supports different actions and jobs so that each class only performs one job and has one reason to change.
This means that the ValidatePerson
class will only be responsible for validating a user, as seen below:
class ValidatePerson {
constructor(name, age) {
this.name = name;
this.age = age;
}
ValidateName(name) {
if (name.length > 3) {
return true;
} else {
return false;
}
}
ValidateAge(age) {
if (age > 18) {
return true;
} else {
return false;
}
}
}
While the new class DisplayPerson
will now be responsible for displaying a person, as you can see in the code block below:
class DisplayPerson {
constructor(name, age) {
this.name = name;
this.age = age;
this.validate = new ValidatePerson(this.name, this.age);
}
Display() {
if (
this.validate.ValidateName(this.name) &&
this.validate.ValidateAge(this.age)
) {
console.log(`Name: ${this.name} and Age: ${this.age}`);
} else {
console.log('Invalid');
}
}
}
With this, you will have fulfilled the single-responsibility principle, meaning our classes now have just one reason to change. If you want to change the DisplayPerson
class, it won’t affect the ValidatePerson
class.
The open-closed principle can be confusing because it's a two-direction principle. According to Bertrand Meyer's definition on Wikipedia, the open-closed principle (OCP) states that software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification.
This definition can be confusing, but an example and further clarification will help you understand.
There are two primary attributes in the OCP:
OCP means that a class, module, function, and other entities can extend their behavior without modifying their source code. In other words, an entity should be extendable without modifying the entity itself. How?
For example, suppose you have an array of iceCreamFlavours
, which contains a list of possible flavors. In the makeIceCream
class, a make()
method will check if a particular flavor exists and logs a message.
const iceCreamFlavors = ['chocolate', 'vanilla'];
class makeIceCream {
constructor(flavor) {
this.flavor = flavor;
}
make() {
if (iceCreamFlavors.indexOf(this.flavor) > -1) {
console.log('Great success. You now have ice cream.');
} else {
console.log('Epic fail. No ice cream for you.');
}
}
}
The code above fails the OCP principle. Why? Well, because the code above is not open to an extension, meaning for you to add new flavors, you would need to directly edit the iceCreamFlavors
array. This means that the code is no longer closed to modification. Haha (that's a lot).
To fix this, you would need an extra class or entity to handle addition, so you no longer need to modify the code directly to make any extension.
const iceCreamFlavors = ['chocolate', 'vanilla'];
class makeIceCream {
constructor(flavor) {
this.flavor = flavor;
}
make() {
if (iceCreamFlavors.indexOf(this.flavor) > -1) {
console.log('Great success. You now have ice cream.');
} else {
console.log('Epic fail. No ice cream for you.');
}
}
}
class addIceCream {
constructor(flavor) {
this.flavor = flavor;
}
add() {
iceCreamFlavors.push(this.flavor);
}
}
Here, we've added a new class — addIceCream
– to handle addition to the iceCreamFlavors
array using the add()
method. This means your code is closed to modification but open to an extension because you can add new flavors without directly affecting the array.
let addStrawberryFlavor = new addIceCream('strawberry');
addStrawberryFlavor.add();
makeStrawberryIceCream.make();
Also, notice that SRP is in place because you created a new class. 😊
In 1987, the Liskov Substitution Principle (LSP) was introduced by Barbara Liskov in her conference keynote “Data abstraction”. A few years later, she defined the principle like this:
“Let Φ(x) be a property provable about objects x of type T. Then Φ(y) should be true for objects y of type S where S is a subtype of T”.
To be honest, that definition is not what many software developers want to see 😂 — so let me break it down into an OOP-related definition.
The principle defines that in an inheritance, objects of a superclass (or parent class) should be substitutable with objects of its subclasses (or child class) without breaking the application or causing any error.
In very plain terms, you want the objects of your subclasses to behave the same way as the objects of your superclass.
A very common example is the rectangle, square scenario. It’s clear that all squares are rectangles because they are quadrilaterals with all four angles being right angles. But not every rectangle is a square. To be a square, its sides must have the same length.
Bearing this in mind, suppose you have a rectangle class to calculate the area of a rectangle and perform other operations like set color:
class Rectangle {
setWidth(width) {
this.width = width;
}
setHeight(height) {
this.height = height;
}
setColor(color) {
// ...
}
getArea() {
return this.width * this.height;
}
}
Knowing fully well that all squares are rectangles, you can inherit the properties of the rectangle. Since the width and height has to be the same, then you can adjust it:
class Square extends Rectangle {
setWidth(width) {
this.width = width;
this.height = width;
}
setHeight(height) {
this.width = height;
this.height = height;
}
}
Taking a look at the example, it should work properly:
let rectangle = new Rectangle();
rectangle.setWidth(10);
rectangle.setHeight(5);
console.log(rectangle.getArea()); // 50
In the above, you will notice that a rectangle is created, and the width and height are set. Then you can calculate the correct area.
But according to the LSP, you want the objects of your subclasses to behave the same way as the objects of your superclass. Meaning if you replace the Rectangle
with Square
, everything should still work well:
let square = new Square();
square.setWidth(10);
square.setHeight(5);
You should get 100, because the setWidth(10)
is supposed to set both the width and height to 10. But because of the setHeight(5)
, this will return 25.
let square = new Square();
square.setWidth(10);
square.setHeight(5);
console.log(square.getArea()); // 25
This breaks the LSP. To fix this, there should be a general class for all shapes that will hold all generic methods that you want the objects of your subclasses to have access to. Then for individual methods, you create an individual class for rectangle and square.
class Shape {
setColor(color) {
this.color = color;
}
getColor() {
return this.color;
}
}
class Rectangle extends Shape {
setWidth(width) {
this.width = width;
}
setHeight(height) {
this.height = height;
}
getArea() {
return this.width * this.height;
}
}
class Square extends Shape {
setSide(side) {
this.side = side;
}
getArea() {
return this.side * this.side;
}
}
This way, you can set the color and get the color using either the super or subclasses:
// superclass
let shape = new Shape();
shape.setColor('red');
console.log(shape.getColor()); // red
// subclass
let rectangle = new Rectangle();
rectangle.setColor('red');
console.log(rectangle.getColor()); // red
// subclass
let square = new Square();
square.setColor('red');
console.log(square.getColor()); // red
The Interface Segregation Principle (ISP) states that “a client should never be forced to implement an interface that it doesn’t use, or clients shouldn’t be forced to depend on methods they do not use”. What does this mean?
Just as the term segregation means — this is all about keeping things separated, meaning separating the interfaces.
Note: By default, JavaScript does not have interfaces, but this principle still applies. So let’s explore this as if the interface exists, so you will know how it works for other programming languages like Java.
A typical interface will contain methods and properties. When you implement this interface into any class, then the class needs to define all its methods. For example, suppose you have an interface that defines methods to draw specific shapes.
interface ShapeInterface {
calculateArea();
calculateVolume();
}
When any class implements this interface, all the methods must be defined even if you won't use them or if they don’t apply to that class.
class Square implements ShapeInterface {
calculateArea(){
//...
}
calculateVolume(){
//...
}
}
class Cuboid implements ShapeInterface {
calculateArea(){
//...
}
calculateVolume(){
//...
}
}
class Rectangle implements ShapeInterface {
calculateArea(){
//...
}
calculateVolume(){
//...
}
}
Looking at the example above, you will notice that you cannot calculate the volume of a square or rectangle. Because the class implements the interface, you need to define all methods, even the ones you won’t use or need.
To fix this, you would need to segregate the interface.
interface ShapeInterface {
calculateArea();
calculateVolume();
}
interface ThreeDimensionalShapeInterface {
calculateArea();
calculateVolume();
}
You can now implement the specific interface that works with each class.
class Square implements ShapeInterface {
calculateArea(){
//...
}
}
class Cuboid implements ThreeDimensionalShapeInterface {
calculateArea(){
//...
}
calculateVolume(){
//...
}
}
class Rectangle implements ShapeInterface {
calculateArea(){
//...
}
}
This principle is targeted towards loosely coupling software modules so that high-level modules (which provide complex logic) are easily reusable and unaffected by changes in low-level modules (which provide utility features).
According to Wikipedia, this principle states that:
In plain terms, this principle states that your classes should depend upon interfaces or abstract classes instead of concrete classes and functions. This makes your classes open to extension, following the open-closed principle.
Let's look at an example. When building a store, you would want your store to make use of a payment gateway like stripe or any other preferred payment method. You might write your code tightly coupled to that API without thinking of the future.
But then what if you discover another payment gateway that offers far better service, let’s say PayPal? Then it becomes a struggle to switch from Stripe to Paypal, which should not be an issue in programming and software design.
class Store {
constructor(user) {
this.stripe = new Stripe(user);
}
purchaseBook(quantity, price) {
this.stripe.makePayment(price * quantity);
}
purchaseCourse(quantity, price) {
this.stripe.makePayment(price * quantity);
}
}
class Stripe {
constructor(user) {
this.user = user;
}
makePayment(amountInDollars) {
console.log(`${this.user} made payment of ${amountInDollars}`);
}
}
Considering the example above, you'll notice that if you change the payment gateway, you won't just need to add the class – you'll also need to make changes to the Store
class. This does not just go against the Dependency Inversion Principle but also against the open-closed principle.
To fix this, you must ensure that your classes depend upon interfaces or abstract classes instead of concrete classes and functions. For this example, this interface will contain all the behavior you want your API to have and doesn't depend on anything. It serves as an intermediary between the high-level and low-level modules.
class Store {
constructor(paymentProcessor) {
this.paymentProcessor = paymentProcessor;
}
purchaseBook(quantity, price) {
this.paymentProcessor.pay(quantity * price);
}
purchaseCourse(quantity, price) {
this.paymentProcessor.pay(quantity * price);
}
}
class StripePaymentProcessor {
constructor(user) {
this.stripe = new Stripe(user);
}
pay(amountInDollars) {
this.stripe.makePayment(amountInDollars);
}
}
class Stripe {
constructor(user) {
this.user = user;
}
makePayment(amountInDollars) {
console.log(`${this.user} made payment of ${amountInDollars}`);
}
}
let store = new Store(new StripePaymentProcessor('John Doe'));
store.purchaseBook(2, 10);
store.purchaseCourse(1, 15);
In the code above, you will notice that the StripePaymentProcessor
class is an interface between the Store
class and the Stripe
class. In a situation where you need to make use of PayPal, all you have to do is create a PayPalPaymentProcessor
which would work with the PayPal
class, and everything will work without affecting the Store
class.
class Store {
constructor(paymentProcessor) {
this.paymentProcessor = paymentProcessor;
}
purchaseBook(quantity, price) {
this.paymentProcessor.pay(quantity * price);
}
purchaseCourse(quantity, price) {
this.paymentProcessor.pay(quantity * price);
}
}
class PayPalPaymentProcessor {
constructor(user) {
this.user = user;
this.paypal = new PayPal();
}
pay(amountInDollars) {
this.paypal.makePayment(this.user, amountInDollars);
}
}
class PayPal {
makePayment(user, amountInDollars) {
console.log(`${user} made payment of ${amountInDollars}`);
}
}
let store = new Store(new PayPalPaymentProcessor('John Doe'));
store.purchaseBook(2, 10);
store.purchaseCourse(1, 15);
You will also notice that this follows the Liskov Substitution Principle because you can replace it with other implementations of the same interface without breaking your application.
It's been an adventure🙃. I hope you noticed that each of these principles are related to the others in some way.
In an attempt to correct one principle, say the dependency inversion principle, you indirectly ensure that your classes are open to extension but closed to modification, for example.
You should keep these principles in mind when writing code, because they make it easier for many people to collaborate on your project. They simplify the process of extending, modifying, testing, and refactoring your code. So make sure you understand their definitions, what they do, and why you need them beyond OOP.
For more understanding, you can watch this video by Beau Carnes on the freeCodeCamp YouTube channel or read this article by Yiğit Kemal Erinç.
Have fun coding!
Original article source at https://www.freecodecamp.org
#solid #oop #javascript
1665818058
In this tutorial, you'll learn about the SOLID principles for Object-Oriented Programming (OOP).
After learning object-oriented programming I have seen people making very subtle mistakes because learning oop only teaches you about the concepts and not how to use it correctly. That's why this course brings you SOLID Principles which gives you the correct way to design your classes and isolate your code well.
What you’ll learn:
Are there any course requirements or prerequisites?
Who this course is for:
#solid #oop #programming
1663321001
The phenomenon of change of matter form one state to another state and back to original state, by altering the conditions of temperature and pressure, is called the interconversion of matter.
https://www.pw.live/chapter-matter-is-our-surrounding-class-9/interconversion-of-states-of-matter
#physics #newton #solid #science #class #scientist #study #education
1661601629
Bài đăng này tiết lộ sáu thư viện giao diện người dùng hàng đầu để sử dụng vào năm 2022. Danh sách này mới và rất khác so với những năm trước.
Danh sách này là từ báo cáo State of JS , một cuộc khảo sát tổng hợp hàng năm về các nhà phát triển trong cộng đồng JavaScript để chia sẻ suy nghĩ của họ về việc sử dụng và sự hài lòng đối với các thư viện, khái niệm và ý kiến xung quanh các công cụ và trong cộng đồng. Báo cáo năm 2021, được phát hành cách đây vài tuần, được hình dung từ việc khảo sát hơn 16.000 nhà phát triển từ khắp nơi trên thế giới.
Đây là những khuôn khổ JS hàng đầu cần chú ý trong năm nay được đánh giá bởi mức độ hài lòng khi sử dụng của nhà phát triển.
SolidJS được cho là framework JS nhanh nhất và có kích thước nhỏ nhất tồn tại cho đến ngày nay. Nó quen thuộc nên bạn đã có ý tưởng về cách sử dụng nó nếu bạn đến từ nền tảng Knockout.js hoặc React Hooks. Nó cung cấp cho bạn quyền kiểm soát hoàn toàn đối với những gì được cập nhật và khi nào, ngay cả ở cấp liên kết DOM mà không có DOM ảo hoặc khác biệt.
Với hỗ trợ JSX và TypeScript, nó biên dịch một lần và sau đó cập nhật khi cần thiết theo cách nhanh nhất có thể. Nó cũng thực sự giàu tính năng với các tính năng tuyệt vời như Fragment, Portals, Context, Suspense, Error Boundaries, Lazy Components, Async & Concurrent Rendering, Implicit Trust, SSR & Hydration, Directives and Streaming.
Svelte.js là một khung JavaScript mã nguồn mở cũng siêu nhanh. Nó thực hiện biên dịch và phục vụ mã tại thời điểm xây dựng chứ không phải thời gian chạy như một số khung công tác khác. Điều này làm cho nó trở nên siêu nhanh, được cho là một trong những khuôn khổ nhanh nhất. Mã được biên dịch ở dạng bit nhỏ hơn và mô-đun JS, và điều này làm cho trình duyệt làm ít hơn và do đó tải nội dung nhanh hơn. Svelte được biết là không có DOM ảo (các thư viện rất nhanh áp dụng cách tiếp cận này), thực sự phản ứng và cho phép bạn viết ít mã hơn với tư cách là một nhà phát triển sử dụng nó.
React là một khuôn khổ JS được nhóm tại Facebook sử dụng để xây dựng giao diện người dùng web, đặc biệt là cho các ứng dụng trang đơn. React rất phản ứng như tên cho thấy — bạn có thể xây dựng các ứng dụng nhỏ đến rất lớn cập nhật DOM mà không cần tải lại trang. Nó được xây dựng để rất nhanh, dễ khai báo và dễ học.
React sử dụng DOM ảo, một cách hay để tạo các ứng dụng web nhanh hơn. Các thành phần cũng có thể dễ dàng được sử dụng lại và một thành phần có thể chứa nhiều thành phần nhỏ hơn trong đó. Việc tạo các ứng dụng động trên web bằng React thay vì Vanilla JS cũng dễ dàng hơn. React có một bộ công cụ giúp bạn thoải mái sử dụng, từ một cộng đồng mạnh mẽ đến các công cụ gỡ lỗi chuyên dụng và tiện ích mở rộng trong các IDE yêu thích của bạn.
Vue.js là một khuôn khổ JS mã nguồn mở được xây dựng bởi Evan You, để xây dựng các giao diện người dùng tương tác. Nó sử dụng kiến trúc model-view-viewmodel, về cơ bản đảm bảo rằng logic độc lập với chế độ xem để mọi thứ rõ ràng và dễ đọc hơn. Vue chú ý tốt đến lớp xem của ứng dụng, sau đó làm việc với các thư viện hỗ trợ cho các chức năng bổ sung khác để lại kích thước ánh sáng lõi.
Như bạn đã biết, Vue làm cho việc thử nghiệm đơn vị trở nên khá dễ dàng. Nó rất linh hoạt và tài liệu cũng là tài liệu dễ đọc nhất, theo ý kiến của tôi, trong số tất cả các khung JS hiện có. Nó cũng rất dễ dàng để bắt đầu. Vue là một trong số ít framework rất phổ biến nhưng không được quản lý bởi bất kỳ tập đoàn lớn nào.
Alpine.js là một công cụ tối thiểu, chắc chắn để soạn thảo hành vi trực tiếp trong phần đánh dấu của bạn. Hãy nghĩ về nó giống như jQuery cho web hiện đại. Nhập một thẻ script và bắt đầu. nó thực sự tối thiểu — nó chỉ có một bộ sưu tập gồm 15 thuộc tính, 6 thuộc tính và 2 phương thức.
“Alpine.js cung cấp cho bạn bản chất phản ứng và khai báo của các khuôn khổ lớn như Vue hoặc React với chi phí thấp hơn nhiều. Bạn có thể giữ DOM của mình và thực hiện hành vi khi bạn thấy phù hợp. " - Caleb Porzio, tác giả dự án
Bạn nên dùng thử. Nó cho phép bạn viết JS bên trong HTML, nội tuyến, mà không cần bất kỳ bản dựng nào hoặc không cần cài đặt nhiều.
Lit là một khuôn khổ JS được xây dựng dựa trên các tiêu chuẩn của các thành phần web, rất tương lai và phản ứng, đồng thời nó chứa các mẫu khai báo và một số tính năng chu đáo để giảm bớt phần soạn sẵn và giúp việc xây dựng các ứng dụng web dễ dàng hơn. Kích thước của tệp nén là 5kb, vì vậy bạn biết thời gian tải sẽ rất ngắn và cũng nhanh chóng. Lit không tải lại các trang mà tự động cập nhật các phần đã thay đổi của DOM — không có cây ảo nào được tạo lại, không có sự khác biệt.
Nó có sân chơi tương tác thực sự thú vị này để thử nghiệm các ý tưởng của bạn. Bạn có thể kiểm tra nó ngay hôm nay.
Trong bài đăng hôm nay, chúng ta đã thấy sáu framework JavaScript hàng đầu để sử dụng trong năm nay, xếp hạng cao trong chỉ số hài lòng của hàng nghìn nhà phát triển JS khác. Các khuôn khổ đáng chú ý khác bao gồm Stimulus, Angular và Ember. Bạn mới nghe nói về cái nào trong số này? Hãy cho tôi biết ở phần bình luận.
Liên kết: https://www.telerik.com/blogs/top-6-frontend-frameworks-2022
#framework #lit #solid #alpine #react #vue
1661594400
Cet article révèle les six principales bibliothèques frontales à utiliser en 2022. La liste est fraîche et très différente des années précédentes.
Cette liste est tirée du rapport State of JS , une enquête annuelle bien organisée auprès des développeurs de la communauté JavaScript pour partager leurs réflexions sur l'utilisation et la satisfaction des bibliothèques, des concepts et des opinions sur les outils et au sein de la communauté. Le rapport 2021, qui a été publié il y a quelques semaines, a été visualisé à partir d'une enquête auprès de plus de 16 000 développeurs du monde entier.
Ce sont les meilleurs frameworks JS à surveiller cette année, classés par la satisfaction de l'utilisation des développeurs.
SolidJS est sans doute le framework JS le plus rapide et le plus petit qui existe aujourd'hui. Il est familier, vous avez donc déjà une idée de la façon de l'utiliser si vous venez d'un arrière-plan Knockout.js ou React Hooks. Il vous offre un contrôle complet sur ce qui est mis à jour et quand, même au niveau de la liaison DOM sans DOM virtuel ni différence.
Avec la prise en charge de JSX et TypeScript, il compile une fois, puis effectue la mise à jour nécessaire de la manière la plus rapide possible. Il est également très riche en fonctionnalités avec des fonctionnalités telles que les fragments, les portails, le contexte, le suspense, les limites d'erreur, les composants paresseux, le rendu asynchrone et simultané, la délégation implicite, le SSR et l'hydratation, les directives et le streaming.
Svelte.js est un framework JavaScript open-source qui est également très rapide. Il effectue la compilation et sert le code au moment de la construction, par opposition à l'exécution comme certains autres frameworks. Cela le rend super rapide, sans doute l'un des frameworks les plus rapides. Le code compilé est en petits bits et modules JS, ce qui fait que le navigateur en fait moins et donc charge le contenu plus rapidement. Svelte est connu pour ne pas avoir de DOM virtuel (les bibliothèques très rapides adoptent cette approche), être vraiment réactif et vous permettre d'écrire moins de code en tant que développeur l'utilisant.
React est un framework JS de l'équipe de Facebook utilisé pour créer des interfaces utilisateur Web, en particulier pour les applications à page unique. React est très réactif comme son nom l'indique - vous pouvez créer des applications petites à très grandes qui mettent à jour le DOM sans rechargement de page. Il a été conçu pour être très rapide, déclaratif et facile à apprendre.
React utilise le DOM virtuel, un bon moyen de créer des applications Web plus rapides. Les composants peuvent également être facilement réutilisés et un composant peut contenir plusieurs composants plus petits. Il est également plus facile de créer des applications dynamiques sur le Web en utilisant React au lieu de Vanilla JS. React dispose d'une suite d'outils qui vous permettent de l'utiliser confortablement, d'une communauté robuste à des outils de débogage dédiés et des extensions dans vos IDE préférés.
Vue.js est un framework JS open source construit par Evan You, pour créer des interfaces utilisateur interactives. Il utilise l'architecture modèle-vue-vuemodèle, garantissant essentiellement que la logique est indépendante de la vue afin que les choses soient plus claires et lisibles. Vue accorde une grande attention à la couche d'affichage de l'application, puis travaille avec les bibliothèques de support pour d'autres fonctionnalités supplémentaires, laissant le noyau léger en taille.
Comme vous pouvez déjà le constater, Vue rend les tests unitaires assez faciles. Il est très flexible et les documents sont également les plus faciles à lire, à mon avis, parmi tous les frameworks JS. Il est également très facile de démarrer. Vue est l'un des rares frameworks qui est très populaire mais qui n'est géré par aucune grande entreprise.
Alpine.js est un outil robuste et minimal pour composer un comportement directement dans votre balisage. Pensez-y comme jQuery pour le Web moderne. Insérez une balise de script et lancez-vous. c'est vraiment minime - il n'a qu'une collection de 15 attributs, 6 propriétés et 2 méthodes.
« Alpine.js vous offre la nature réactive et déclarative des grands frameworks comme Vue ou React à un coût bien moindre. Vous pouvez garder votre DOM et saupoudrer de comportement comme bon vous semble. – Caleb Porzio, auteur du projet
Tu devrais l'essayer. Il vous permet d'écrire JS à l'intérieur du HTML, en ligne, sans aucune construction ni besoin de beaucoup d'installation.
Lit est un framework JS construit sur des standards de composants Web, très futuriste et réactif, et il contient des modèles déclaratifs et une poignée de fonctionnalités réfléchies pour réduire le passe-partout et faciliter la création d'applications Web. La taille est de 5 Ko pour le fichier compressé, vous savez donc que le temps de chargement sera très court et qu'il est également rapide. Lit ne recharge pas les pages mais met à jour dynamiquement les parties modifiées du DOM - pas de reconstruction d'arborescence virtuelle, pas de différence.
Il a ce terrain de jeu interactif vraiment cool pour tester vos idées. Vous pouvez le vérifier aujourd'hui.
Dans l'article d'aujourd'hui, nous avons vu les six meilleurs frameworks JavaScript à utiliser cette année qui se classent en tête de l'indice de satisfaction de milliers d'autres développeurs JS. D'autres frameworks notables incluent Stimulus, Angular et Ember. Laquelle de celles-ci entendez-vous parler pour la première fois ? Faites-moi savoir dans les commentaires.
Lien : https://www.telerik.com/blogs/top-6-frontend-frameworks-2022
#framework #lit #solid #alpine #react #vue
1661587140
A front end framework is essentially a bundle of JavaScript code that someone else has written which you can include in your application to help you build it faster. It’s somewhat analogous to the framework for a house—which might include instructions on how to build windows, doors, or certain rooms. The house’s developer can then decide how to take the rules the framework gave them and build the house in the manner they desire.
Front end frameworks declare rules on how developers should structure their HTML, CSS, and JavaScript, to make the application easier to develop. In this manner, frameworks are opinionated—they prefer you to make certain decisions. Sure, you can choose to not listen to the framework and write the JavaScript however you want (unopinionated), but then you’re likely to get certain details wrong and run into issues that the frameworks were specifically developed to solve.
In today’s post, we have seen the top six JavaScript frameworks to use this year that rank high in the satisfaction index by thousands of other JS developers. Other notable frameworks include Stimulus, Angular and Ember. Which of these are you just hearing about for the first time?
See more at: https://www.telerik.com/blogs/top-6-frontend-frameworks-2022
#framework #lit #solid #alpine #react #vue
1661579775
這篇文章揭示了 2022 年使用的前六個前端庫。這個列表是新鮮的,與往年有很大不同。
此列表來自JS 狀態報告,該報告是對 JavaScript 社區開發人員的年度綜合調查,旨在分享他們對庫的使用和滿意度的看法,以及圍繞工具和社區內的概念和意見。幾週前發布的 2021 年報告是通過對來自世界各地的 16,000 多名開發人員的調查得出的。
這些是今年開發人員使用滿意度最值得關注的 JS 框架。
SolidJS可以說是當今存在的最快和最小的 JS 框架。它很熟悉,因此如果您來自 Knockout.js 或 React Hooks 背景,您已經知道如何使用它。它使您可以完全控制更新的內容和時間,即使在沒有虛擬 DOM 或差異的 DOM 綁定級別也是如此。
借助 JSX 和 TypeScript 支持,它編譯一次,然後根據需要以最快的方式進行更新。它還具有非常豐富的功能,例如片段、門戶、上下文、懸念、錯誤邊界、惰性組件、異步和並發渲染、隱式委託、SSR 和水化、指令和流式傳輸。
Svelte.js是一個開源的 JavaScript 框架,速度也非常快。它在構建時進行編譯並提供代碼,而不是像其他一些框架那樣運行時。這使它超級快,可以說是最快的框架之一。編譯後的代碼是更小的位和 JS 模塊,這使得瀏覽器做的更少,因此加載內容更快。眾所周知,Svelte 沒有虛擬 DOM(非常快的庫採用這種方法),具有真正的反應性,並且允許您作為開發人員使用它編寫更少的代碼。
React是 Facebook 團隊的一個 JS 框架,用於構建 Web 用戶界面,尤其是用於單頁應用程序。顧名思義,React 非常具有反應性——您可以構建從小型到大型的應用程序來更新 DOM 而無需重新加載頁面。它被構建為非常快速、聲明性和易於學習。
React 使用虛擬 DOM,這是創建更快的 Web 應用程序的好方法。組件也可以很容易地重複使用,一個組件中可以包含多個較小的組件。使用 React 而不是 Vanilla JS 在 Web 上創建動態應用程序也更容易。React 有一套工具讓你使用起來很舒服,從一個強大的社區到你最喜歡的 IDE 中的專用調試工具和擴展。
Vue.js是由 Evan You 構建的開源 JS 框架,用於構建交互式用戶界面。它採用了model-view-viewmodel的架構,基本保證了邏輯獨立於視圖,讓事情更加清晰易讀。Vue 非常關注應用程序的視圖層,然後與支持庫一起工作以提供其他附加功能,從而保持核心的大小。
正如您已經知道的那樣,Vue 使單元測試變得非常容易。它非常靈活,而且在我看來,在所有 JS 框架中,文檔也是最容易閱讀的。它也非常容易上手。Vue 是少數幾個非常流行但不受任何大公司管理的框架之一。
Alpine.js是一個堅固的、最小的工具,用於直接在您的標記中編寫行為。把它想像成現代網絡的 jQuery。插入一個腳本標籤,然後開始吧。它真的很小——它只有 15 個屬性、6 個屬性和 2 個方法的集合。
“Alpine.js 以低得多的成本為您提供了 Vue 或 React 等大型框架的反應性和聲明性。你可以保留你的 DOM,並在你認為合適的時候加入一些行為。” – Caleb Porzio,項目作者
你應該試試看。它允許您在 HTML 中內聯編寫 JS,無需任何構建或大量安裝。
Lit是一個基於 Web 組件標準構建的 JS 框架,非常具有未來感和反應性,它包含聲明性模板和一些經過深思熟慮的功能,以減少樣板文件並使構建 Web 應用程序更容易。壓縮文件的大小為 5kb,因此您知道加載時間會很短,而且速度也很快。Lit 不會重新加載頁面,而是動態更新 DOM 的更改部分——沒有虛擬樹重建,沒有差異。
它有一個非常酷的交互式遊樂場來測試你的想法。你今天可以檢查一下。
在今天的帖子中,我們看到了今年使用的排名前六的 JavaScript 框架,它們在成千上萬的其他 JS 開發人員的滿意度指數中排名靠前。其他值得注意的框架包括 Stimulus、Angular 和 Ember。您是第一次聽說以下哪些?在評論中告訴我。
鏈接:https ://www.telerik.com/blogs/top-6-frontend-frameworks-2022
#framework #lit #solid #alpine #react #vue
1661572473
This post reveals the top six frontend libraries to use in 2022. The list is fresh and very different from the previous years.
This list is from the State of JS report, an annual well-put-together survey of developers in the JavaScript community to share their thoughts around usage and satisfaction of libraries, concepts and opinions around tools and within the community. The 2021 report, which was released some weeks ago, was visualized from surveying over 16,000 developers from across the world.
These are the top JS frameworks to look out for this year rated by developer usage satisfaction.
SolidJS is arguably the fastest and smallest sized JS framework that exists today. It is familiar so you already have an idea of how to use it if you are coming from a Knockout.js or a React Hooks background. It provides you complete control over what gets updated and when, even at the DOM binding level without Virtual DOMs or diffing.
With JSX and TypeScript support, it compiles once and then does updating as needed in the fastest possible way. It is also really feature-rich with great features like Fragments, Portals, Context, Suspense, Error Boundaries, Lazy Components, Async & Concurrent Rendering, Implicit Delegation, SSR & Hydration, Directives and Streaming.
Svelte.js is an open-source JavaScript framework that is also super fast. It does compilation and serves the code at build time as opposed to runtime like some other frameworks. This makes it super fast, arguably one of the fastest frameworks. The compiled code is in smaller bits and JS modules, and this makes the browser do less and so load content faster. Svelte is known to have no virtual DOM (very fast libraries adopt this approach), be really reactive and allow you to write less code as a developer using it.
React is a JS framework by the team at Facebook used to build web user interfaces, especially for single page applications. React is very reactive as the name suggests—you can build small to very large applications that updates the DOM without page reloads. It was built to be very fast, declarative and easy to learn.
React uses virtual DOM, a good way to create faster web apps. The components can also easily be reused, and a component can contain multiple smaller components in it. It is also easier to create dynamic applications on the web using React instead of Vanilla JS. React has a suite of tools that make you comfortable using it, from a robust community to dedicated debugging tools and extensions in your favorite IDEs.
Vue.js is an open-source JS framework built by Evan You, for building interactive user interfaces. It uses the model-view-viewmodel architecture, basically ensuring that logic is independent of the view so that things are clearer and legible. Vue pays good attention to the view layer of the application, and then works with supporting libraries for other additional functionality leaving the core light in size.
As you can already tell, Vue makes unit testing pretty easy. It is very flexible, and the docs also are the easiest to read, in my opinion, among all JS frameworks out there. It is also super easy to get started with. Vue is one of the few frameworks that is very popular but not managed by any big corporations.
Alpine.js is a rugged, minimal tool for composing behavior directly in your markup. Think of it like jQuery for the modern web. Plop in a script tag and get going. it is really minimal—it only has a collection of 15 attributes, 6 properties and 2 methods.
“Alpine.js offers you the reactive and declarative nature of big frameworks like Vue or React at a much lower cost. You get to keep your DOM, and sprinkle in behavior as you see fit.” – Caleb Porzio, project author
You should try it out. It lets you write JS inside the HTML, inline, without any builds or need for a lot of installation.
Lit is a JS framework built on web components standards, very futuristic and reactive, and it contains declarative templates and a handful of thoughtful features to reduce boilerplate and make building web apps easier. The size is 5kb for the compressed file, so you know the load time will be very short, and it is also fast. Lit does not reload pages but dynamically updates the changed parts of the DOM—no virtual tree rebuilds, no diffing.
It has this really cool interactive playground to test out your ideas. You can check it out today.
In today’s post, we have seen the top six JavaScript frameworks to use this year that rank high in the satisfaction index by thousands of other JS developers. Other notable frameworks include Stimulus, Angular and Ember. Which of these are you just hearing about for the first time? Let me know in the comments.
Link: https://www.telerik.com/blogs/top-6-frontend-frameworks-2022
#framework #lit #solid #alpine #react #vue
1660881900
React-like solid.js browser router with hassle-free nesting / dynamic routes
Solid.js default solid-app-router package does not support convenient work with route nesting / dynamic routes. Docs says, it only supports <Outlet/>
rendering, i.e.:
<Route path='/' element={<div>Onion starts here <Outlet /></div>}>
<Route path='layer1' element={<div>Another layer <Outlet /></div>}>
<Route path='layer2' element={<div>Innermost layer</div>}></Route>
</Route>
</Route>
So, as you can see, it's not really convenient and component/reactive way. And I've decided to write a better lib, i.e.:
<Routes>
<Route path={'/'}>
<div>Onion starts here <Onion /></div>
</Route>
</Routes>
// for /*
const Onion = () => (
<Routes>
<Route path={'layer1'}>
<div>Another layer <Onion2 /></div>
</Route>
</Routes>
)
// for /layer1/*
const Onion2 = () => (
<Routes>
<Route path={'layer2'}>
<div>Innermost layer</div>
</Route>
</Routes>
)
For this example the original way looks better, but if we wanna be honest, the real life case is:
You kinda coding with solid-app-router:
const App = () => (
<Router>
<Route path={'/home'} element={<HomePage/>}/>
<Route path={'/personal-account'} element={<PersonalAccountPage/>}/>
<Route path={'/'} element={<Navigate href={'/home'}/>}/>
</Router>
)
const HomePage = () => (
<>
<div>static HomePage info</div>
<Link href={'/personal-account'}>Go to personal account</Link>
</>
)
const PersonalAccountPage = () => {
const [clicks, setClicks] = createSignal(0)
const pages = createMemo(() => [
{
href: 'products',
name: 'Products',
component: ProductsComponent,
props: {clicks: clicks()}
},
{
href: 'billing',
name: 'Billing',
component: BillingComponent,
props: {clicks: clicks()},
},
])
return (
<>
<div class={'nav left-part'}>
<For each={pages()}>
{page => (
<Link href={page.href} class={'w-full'}>{page.name}</Link>
)}
</For>
<button onClick={() => setClicks(clicks() + 1)}>Click me</button>
</div>
<div class={'container right-part'}>
<For each={pages()}>
{page => (
<Route path={page.href} element={(<page.component {...page.props}/>)}/>
)}
</For>
</div>
</>
)
}
And then the problems begin, "container right-part" routing just won't work, and you will read the docs and come to a conclusion:
You MUST define routes only in a static way.
And then rewrite everything with extremely shitty <Outlet/>
strategy and lots of cross-app storages, if at all possible.
So, the only purpose of this library is to make routing workable and convenient.
npm i @gh0st-work/solid-js-router
On the same example:
import {Routes, Route, Link, Router, DefaultRoute} from '@gh0st-work/solid-js-router';
const App = () => (
<Router>
<Routes>
<Route path={'/home'} fallback={true}>
<HomePage/>
</Route>
<Route path={'/personal-account'}>
<PersonalAccountPage/>
</Route>
<DefaultRoute to={'/home'}/>
</Routes>
</Router>
)
const HomePage = () => (
<>
<div>static HomePage info</div>
<Link href={'/personal-account'}>Go to personal account</Link>
</>
)
const PersonalAccountPage = () => {
const [clicks, setClicks] = createSignal(0)
const pages = createMemo(() => [
{
href: 'products',
name: 'Products',
component: ProductsComponent,
props: {clicks: clicks()}
},
{
href: 'billing',
name: 'Billing',
component: BillingComponent,
props: {clicks: clicks()},
},
])
return (
<>
<div class={'nav left-part'}>
<For each={pages()}>
{page => (
<Link href={page.href} class={'w-full'}>{page.name}</Link>
)}
</For>
<button onClick={() => setClicks(clicks() + 1)}>Click me</button>
</div>
<div class={'container right-part'}>
<Routes>
<For each={pages()}>
{page => (
<Route path={page.href}>
<page.component {...page.props}/>
</Route>
)}
</For>
<DefaultRoute to={'products'}/>
</Routes>
</div>
</>
)
}
And it just works perfectly.
<Router>
Component for global routing management, use in only once, wrap your app in it.
Props:
createBrowserHistory()
instance<Routes>
Component for defining your routes, just wrap them in it.
Props:
({route, parentRoute}) => {}
that will be called on every route change<Route>
components will be ignored<Route>
Just route component.
Props:
/
, i.e. /personal-account
-> /products
.true
/false
).fallback={true}
route will be used.Matching supported, child must be function:
import {Routes, Route, Link, Router, DefaultRoute, Navigate} from '@gh0st-work/solid-js-router';
const App = () => (
<Router>
<Routes>
<Route path={'/car/:id'}>
{({id}) => <Car id={id}/>}
</Route>
<Route path={'/home'}>
<Navigate to={'/car/1'}/>
</Route>
<DefaultRoute to={'/home'}/>
</Routes>
</Router>
)
const Car = ({id}) => {
return (<span>Car #{id}</span>)
}
<Navigate>
Component that will redirect on mount.
Props:
<DefaultRoute>
Just shortcut to:
<Route path={'*'}>
<Navigate to={to}/>
</Route>
Insert it in the end of your routes and get rid of fallbacks.
Props:
*
regex matching)<Link>
<a>
tag with e.preventDefault()
, to use this routing.
Props:
({href, e}) => {}
({href, e}) => {}
<a>
declaration.Ex:
import {Link} from "@gh0st-work/solid-js-router";
const PersonalAccount = () => {
return (
<>
<Link
href={'/home'}
class={'font-medium text-amber-500 hover:text-amber-400'}
beforeRedirect={({href, e}) => console.log(href, e)}
afterRedirect={({href, e}) => console.log(href, e)}
>
Go home
</Link>
<Link
href={'/home'}
class={'text-white font-medium text-lg bg-amber-500 hover:bg-amber-400 flex items-center justify-center space-x-2 rounded-md px-4 py-2'}
>
<i class={'w-4 h-4 fa-solid fa-house'}/>
<span>Go home button</span>
</Link>
</>
)
}
useHistory()
History navigation, all apis from history package.
And history.pathname()
(signal) used in routing.
Ex:
import {useHistory} from "@gh0st-work/solid-js-router";
const Home = () => {
const history = useHistory()
return (
<>
<span>Current pathname: {history.pathname()}</span>
<button onClick={() => history.push('/home')}>Go home</button>
<button onClick={() => history.back()}>Go back</button>
</>
)
}
Author: gh0st-work
Source code: https://github.com/gh0st-work/solid-js-router
#react #javascript #solid
1660763160
Felte is a simple to use form library for Svelte, Solid and React. No Field
or Form
components are needed, just plain stores and actions to build your form however you like. You can see it in action in this CodeSandbox demo!
name
attribute is necessary).reporter
packages.<script>
import { createForm } from 'felte';
const { form } = createForm({
onSubmit: async (values) => {
/* call to an api */
},
});
</script>
<form use:form>
<input type="text" name="email" />
<input type="password" name="password" />
<button type="submit">Sign In</button>
</form>
import { createForm } from '@felte/solid';
function Form() {
const { form } = createForm({
onSubmit: async (values) => {
/* call to an api */
},
});
return (
<form use:form>
<input type="text" name="email" />
<input type="password" name="password" />
<button type="submit">Sign In</button>
</form>
);
}
import { useForm } from '@felte/react';
// if using preact, use `@felte/preact`
function Form() {
const { form } = useForm({
onSubmit: async (values) => {
/* call to an api */
},
});
return (
<form ref={form}>
<input type="text" name="email" />
<input type="password" name="password" />
<button type="submit">Sign In</button>
</form>
);
}
<script type="module">
import 'https://unpkg.com/@felte/element@0.4.0/dist/min/felte-form.js';
const felteForm = document.querySelector('felte-form');
felteForm.configuration = {
onSubmit: async (values) => {
console.log(values);
},
};
</script>
<felte-form>
<form>
<input type="text" name="email" />
<input type="password" name="password" />
<button type="submit">Sign In</button>
</form>
</felte-form>
This example works without a bundler! Copy its contents to an HTML file and open it on your browser. A more complete example like this, with validation and error reporting, can be found here.
You can find fully functional examples on the /examples directory of this repository. You should be able to open them on CodeSandbox by replacing github's url to githubbox
. E.g. Replace https://github.com/pablo-abc/felte/tree/main/examples/svelte/basic
with https://githubbox.com/pablo-abc/felte/tree/main/examples/svelte/basic
.
This repository is a mono-repo containing multiple packages located in the packages
directory. Maintained using pnpm and Changesets.
We provide two packages that are specific to Svelte:
This is the core package that contains all the basic functionality you need to handle your forms in Svelte. Felte optionally allows you to use error reporters (see them as plugins) to prevent you from needing to find a way to display your errors on your form manually. For this we provide already some reporter packages contained in this same repo.
A reporter package that uses a Svelte component to pass the validation messages for you to display. This provides an API that might feel the most familiar to most developers.
We provide two packages that are specific to Solid:
This is the core package that contains all the basic functionality you need to handle your forms in Solid. Same as felte
but specifically made for Solid.
A reporter package that uses a Solid component to pass the validation messages for you to display. This provides an API that might feel the most familiar to most developers.
We provide two packages that are specific to React:
This is the main package that contains the basic functionality you need to handle your forms in React. Same as felte
but specifically made for React.
A reporter packages that uses a React component to pass the validation messages for you to display. This provides an API that might feel the most familiar to most developers.
We provide two packages that are specific to Preact:
This is the main package that contains the basic functionality you need to handle your forms in Preact. Same as felte
but specifically made for Preact. The API is the same as @felte/react
so you can refer to the same documentation.
A reporter packages that uses a Preact component to pass the validation messages for you to display. This provides an API that might feel the most familiar to most developers. The API is the same as @felte/react
so you can refer to the same documentation.
We provide three packages that can be used with only VanillaJS. Two of them using Web Components. These elements do not use the shadow DOM since there is no reason to isolate styles.
This is the main package that contains the basic functionality you need to handle your forms in vanilla JS using a custom element. Similar to felte
but specifically made to be used as a custom element. This is the recommended way to handle your forms when using Vanilla JS. Web components are well supported by all major browsers so this should be a safe option unless you need to support legacy browsers.
A reporter packages that uses a custom element to display validation messages on the DOM. This the recommended way to display your validation messages when using vanilla JS.
This is the main package that contains the basic functionality you need to handle your forms in vanilla JS. Similar to felte
and other integrations but with all code related to frameworks removed. This requires a bit more work to use, since you'll be the one in charge of cleaning up subscribers and listeners on it. It's API is basically the same as felte
(Svelte's integration) so you can use Svelte's documentation as a reference. This can be used as a starting point to create your own integration/package for other environments. When it comes to vanilla JS we'd recommend using @felte/element
using web components.
The following packages can be used with any of the framework specific felte
wrappers:
A utility package to help you validate your form with Yup.
A utility package to help you validate your form with Zod.
A utility package to help you validate your form with Superstruct.
A utility package to help you validate your form with Vest.
The following packages can be used with any of the framework specific felte
wrappers:
A reporter that uses Tippy.js to display your validation messages without needing any extra work.
A reporter that uses the browser's constraint validation API to display your validation messages.
A reporter that displays the error messages in the DOM, either as a single element or a list of elements.
If you want to contribute to this project you may check CONTRIBUTING.md
for general guidelines on how to do so.
Author: pablo-a
Source code: https://github.com/pablo-abc/felte
License: MIT license
#svetle #javascript #solid #react
1660626840
Solid Forms provides several form control objects useful for making working with forms easier. Demos and examples below.
# solidjs
yarn add solid-forms
# or
npm install solid-forms
Note: Solid Forms is brand new and should be considered "beta" until the version release hits 1.0.0
. This means breaking changes can come in minor releases. We are following semantic versioning.
The basic building block of Solid Forms are FormControls (see IFormControl
API). A FormControl is intended to model a single input element of a form. For example, an <input />
element or a radio button group. You can use a FormControl to save the value of the input, to handle validation and track errors, to track whether the input has been touched, changed, submitted, etc. Importantly, the FormControl itself is just a Solidjs Store
object so all of it's properties are observable and you can easily respond to changes (e.g. with createEffect()
or just using the control values inside of a component directly). All form controls in solid-forms
are immutable unless you use the provided methods to update their state.
For example (Stackblitz demo),
import { Show, mergeProps, type Component } from "solid-js";
import { createFormControl } from "solid-forms";
export const TextInput: Component<{
control?: IFormControl<string>;
name?: string;
type?: string;
}> = (props) => {
// here we provide a default form control in case the user doesn't supply one
props = mergeProps({ control: createFormControl(""), type: "text" }, props);
return (
<div
classList={{
"is-invalid": !!props.control.errors,
"is-touched": props.control.isTouched,
"is-required": props.control.isRequired,
}}
>
<input
name={props.name}
type={props.type}
value={props.control.value}
oninput={(e) => {
props.control.setValue(e.currentTarget.value);
}}
onblur={() => props.control.markTouched(true)}
required={props.control.isRequired}
/>
<Show when={props.control.isTouched && props.control.errors?.isMissing}>
<small>Answer required.</small>
</Show>
</div>
);
};
But the real power of FormControls comes from their composability with other controls such as FormGroups (see IFormGroup
API) and FormArrays (see IFormArray
API).
For example (Stackblitz demo),
import { Show, mergeProps, createEffect, type Component } from "solid-js";
import { createFormGroup, createFormControl } from "solid-forms";
// here we import the TextInput component we defined above
import { TextInput } from "./TextInput";
export const ExampleForm: Component<{}> = () => {
const group = createFormGroup({
name: createFormControl(""),
email: createFormControl("", {
required: true,
validators: (value: string) =>
value.length === 0 ? { isMissing: true } : null,
}),
});
// This will automatically re-run whenever `group.isDisabled`, `group.isValid` or `group.value` change
createEffect(() => {
if (group.isDisabled || !group.isValid) return;
console.log("Current group value", group.value);
});
const onSubmit = async () => {
if (group.isSubmitted || !group.isValid) return;
group.markSubmitted(true);
// do stuff...
// const { name, email } = group.value;
};
return (
<form onSubmit={onSubmit}>
<label for="name">Your name</label>
<TextInput name="name" control={group.controls.name} />
<label for="email">Your email address</label>
<TextInput name="email" type="email" control={group.controls.email} />
<button>Submit</button>
</form>
);
};
Lets begin by looking at how to use FormControls to model individual form fields, then we'll learn how to use FormGroups and FormArrays to model forms and form fieldsets (i.e. partial forms).
See IFormControl in the API section for all properties and methods.
To model individual form fields, we'll use FormControls created via createFormControl()
. We can create a FormControl with an initial value of ""
like so:
const control = createFormControl("");
That's the same as this (learn more about these options in the API reference):
const control = createFormControl("", {
id: Symbol("Control-1"),
data: undefined,
disabled: false,
touched: false,
dirty: false,
readonly: false,
submitted: false,
pending: false,
errors: null,
validators: undefined,
});
If we want to set the value on our FormControl we can do the following (note, all control objects in solid-forms
are immutable so you need to use the provided methods--e.g. setValue()
--to update their state):
control.setValue("Hi");
control.value; // "Hi"
We can also mark our FormControl as touched (or required, disabled, readonly, submitted, pending, or dirty).
control.markTouched(true);
control.touched; // true
control.markTouched(false); // you get the idea
We can manually add errors to our FormControl
control.errors; // null
control.isValid; // true
control.setErrors({ required: true });
control.errors; // { required: true }
control.isValid; // false
control.patchErrors({ tooLong: "must be less than 5 characters" });
control.errors; // { required: true, tooLong: "must be less than 5 characters" }
We can add a validation function (or functions) which will be run after every value change
control.value; // ""
control.errors; // null
control.setValidators((value) =>
typeof value === "string" && value.length === 0 ? { isMissing: true } : null
);
control.value; // ""
control.errors; // { isMissing: true }
control.setValue("Hi");
control.errors; // null
Under-the-hood, FormControls are just Solidjs stores so we also have the ability to observe any changes to those properties with Solidjs.
createEffect(() => {
console.log("Value change: ", control.value);
});
// here we manually run validation inside of a render effect
createRenderEffect(() => {
if (control.value.toLowerCase() !== control.value) {
control.setErrors({ mustBeLowercase: true });
} else {
control.setErrors(null);
}
});
<div classList={{ "is-invalid": !!control.errors }}>
<p>The control's current value is: {JSON.stringify(control.value)}</p>
</div>;
You can see all the IFormControl properties and methods in the API section. Lets look at creating a reusable form field component with an input.
Lets revist our TextInput
example from above (Stackblitz demo),
import { Show, For, mergeProps, type Component } from "solid-js";
import { createFormControl } from "solid-forms";
export const TextInput: Component<{
control?: IFormControl<string>;
name?: string;
type?: string;
}> = (props) => {
// here we provide a default form control in case the user doesn't supply one
props = mergeProps({ control: createFormControl(""), type: "text" }, props);
return (
<div
classList={{
"is-invalid": !!props.control.errors,
"is-touched": props.control.isTouched,
"is-required": props.control.isRequired,
"is-disabled": props.control.isDisabled,
}}
>
<input
name={props.name}
type={props.type}
value={props.control.value}
oninput={(e) => {
props.control.setValue(e.currentTarget.value);
}}
onblur={() => props.control.markTouched(true)}
required={props.control.isRequired}
disabled={props.control.isDisabled}
/>
<Show when={props.control.isTouched && !props.control.isValid}>
<For each={Object.values(props.control.errors)}>
{(errorMsg: string) => <small>{errorMsg}</small>}
</For>
</Show>
</div>
);
};
Breaking this example down: we'd like the ability for a parent component to pass in a FormControl for the TextInput
to use, but we'd also like the TextInput
to be usable on its own if the parent doesn't supply a control
value. We can accomplish that just like any other Solidjs component using mergeProps
from the Solidjs core library to provide a default control
prop value if the user doesn't supply one.
export const TextInput: Component<{
control?: IFormControl<string>;
name?: string;
type?: string;
}> = (props) => {
props = mergeProps({ control: createFormControl(""), type: "text" }, props);
// ...
};
Since FormControl (and FormGroups and FormArrays) are just Solidjs stores
under-the-hood, we can easily use the core classList
prop to add css classes to our TextInput
if it is invalid, touched, required, or disabled.
// ...
return (
<div
classList={{
"is-invalid": !!props.control.errors,
"is-touched": props.control.isTouched,
"is-required": props.control.isRequired,
"is-disabled": props.control.isDisabled,
}}
>
We set the underlying <input />
element to be equal to the FormControl's value (value={props.control.value}
), we react to input value changes and update the FormControl's value (props.control.setValue(e.currentTarget.value)
), we mark the control as touched on blur events, and we setup the input to be required and disabled if the FormControl is required or disabled.
<input
name={props.name}
type={props.type}
value={props.control.value}
oninput={(e) => {
props.control.setValue(e.currentTarget.value);
}}
onblur={() => props.control.markTouched(true)}
required={props.control.isRequired}
disabled={props.control.isDisabled}
/>
Finally, we decide to show errors associated with this FormControl if the control isn't valid AND if the control has been touched. When this happens, we show all the error messages associated with the control.
<Show when={props.control.isTouched && !props.control.isValid}>
<For each={Object.values(props.control.errors)}>
{(errorMsg: string) => <small>{errorMsg}</small>}
</For>
</Show>
Validating form data and working with errors is a core part of handling user input. The are two primary ways of validating data in Solid Forms. The simple but more limited approach is to use validator functions. The more flexible and powerful approach is to just use Solidjs built-ins like createEffect()
to observe control changes and then setErrors()
and patchErrors()
on a control as appropriate.
Validator functions are optional functions you can provide to a control which are run whenever the control's value changes (technically rawValue
) and either return null
(if there are no errors) or return an object with key-value entries if there are errors.
import { type ValidatorFn } from "solid-forms";
const requiredValidator: ValidatorFn = (rawValue: string) =>
rawValue.length === 0 ? { isMissing: true } : null;
const lowercaseValidator: ValidatorFn = (rawValue: string) =>
rawValue.toLowerCase() !== rawValue ? { isNotLowercase: true } : null;
// You can also create controls with validator functions
const control = createFormControl("", {
validators: [requiredValidator, lowercaseValidator],
});
In this example, we provide a validator function that returns an error if the control's value is length 0
and a separate function which errors if the input string is not all lowercase. If we provide multiple validator functions to a control, all validator functions will be run on every change and their errors will be merged together.
You can update the validator function of a control with control.setValidators()
.
Validator functions are a nice and quick way to add validation to your controls, but a more powerful approach is to observe control changes and manually set errors. With this approach, we have access to the full array of control properties which we can include in our validation logic (validator functions just have access to the control's value).
For example:
const control = createFormControl("");
createRenderEffect(() => {
if (control.value.includes("@")) {
control.setErrors({ mustNotIncludeSymbol: "Cannot include '@' symbol." });
} else {
control.setErrors(null);
}
});
Here we observe control value changes and set an error if the value includes an "@"
symbol or else clear the errors (to indicate that the value is valid).
If we have multiple different validation effects, we should use the source
property of control.setErrors()
and control.patchErrors()
to partition the errors associated with each effect.
For example (Stackblitz demo),
const control = createFormControl("");
createRenderEffect(() => {
const source = "@ validator";
if (control.value.includes("@")) {
control.setErrors(
{ mustNotIncludeSymbol: "Cannot include '@' symbol." },
{ source }
);
} else {
control.setErrors(null, { source });
}
});
const source = Symbol("Max length validator");
createRenderEffect(() => {
if (control.value.length > 10) {
control.setErrors(
{ tooLong: "Cannot be more than 10 characters." },
{ source }
);
} else {
control.setErrors(null, { source });
}
});
Here when we use control.setErrors()
and also provide the source
option, we will only clear or overwrite existing errors that were also set by the same "source". Another way we could have accomplished this would be by using control.patchErrors()
. The patchErrors method merges its changes into the existing control.errors
property, rather than replacing the existing errors. If you pass an errors object with a key who's value is null
, then that key will be deleted from the control errors.
For example:
createRenderEffect(() => {
if (control.value.includes("@")) {
control.patchErrors({ mustNotIncludeSymbol: "Cannot include '@' symbol." });
} else {
control.patchErrors({ mustNotIncludeSymbol: null });
}
});
However, in this case using control.patchErrors()
isn't as good an approach as using the "source" option. An accidental use of control.setErrors()
elsewhere would still overwrite this render effect's validation (which wouldn't happen if you used "source").
Also note, we're using createRenderEffect()
here (rather than createEffect()
) since validation and errors are likely to affect the DOM (by displaying errors to the user).
Also note, when performing async validation you can mark a control as "pending" via control.markPending(true)
to indicate that there is pending validation. control.isValid
is only true if the control both doesn't have any errors and is also not pending. The control.markPending()
method also accepts a source
option for partitioning the pending state of different validation effects. A control will be considered pending so long as any "source" is still pending.
See IFormGroup in the API section for all properties and methods.
To model a form (or fieldset) with multiple form fields, we'll use FormGroups created via createFormGroup()
. A FormGroup has all of the properties that a FormControl has, but it also has additional properties like a controls
property which contains the FormGroup's child controls.
At it's simplest, we can create a FormGroup with no children and the default state.
const group = createFormGroup();
This is the same as:
const group = createFormGroup(
{},
{
id: Symbol("Control-1"),
data: undefined,
disabled: false,
touched: false,
dirty: false,
readonly: false,
submitted: false,
pending: false,
errors: null,
validators: undefined,
}
);
If we want to add two child FormControls representing a person's name to our FormGroup we could do the following (note, all control objects in solid-forms
are immutable so you need to use the provided methods--e.g. setControl()
--to update their state):
group.setControl("firstName", createFormControl(""));
group.controls; // { firstName: IFormControl<string> }
group.value; // { firstName: "" }
group.setControl("lastName", createFormControl(""));
group.controls; // { firstName: IFormControl<string>, lastName: IFormControl<string> }
group.value; // { firstName: "", lastName: "" }
We can also set all the FormGroup's controls at once with
group.setControls({
firstName: createFormControl("John"),
lastName: createFormControl("Carroll"),
});
group.controls; // { firstName: IFormControl<string>, lastName: IFormControl<string> }
group.value; // { firstName: "John", lastName: "Carroll" }
group.removeControl("firstName");
group.controls; // { lastName: IFormControl<string> }
group.value; // { lastName: "Carroll" }
If we want to set the value of our FormGroup we can:
group.setValue({ firstName: "Sandy", lastName: "Smith" });
group.value; // { firstName: "Sandy", lastName: "Smith" }
Note that FormGroup's derive their value from the value of their enabled child controls. When you use group.setValue()
you are really setting the values of the FormGroup's children.
group.controls.firstName.value; // "Sandy"
group.controls.lastName.value; // "Smith"
You can use patchValue()
to update the value of some controls but not others
group.patchValue({ lastName: "Carroll" });
group.value; // { firstName: "Sandy", lastName: "Carroll" }
group.controls.firstName.value; // "Sandy"
group.controls.lastName.value; // "Carroll"
Since a FormGroup's value is equal to the value of all their enabled children, is a child is disabled then it's value will be excluded from group.value
.
group.value; // { firstName: "Sandy", lastName: "Carroll" }
group.controls.firstName.markDisabled(true);
group.value; // { lastName: "Carroll" }
If we'd like to get the FormGroup's value ignoring the disabled status of children, we can use the group.rawValue
property.
group.rawValue; // { firstName: "Sandy", lastName: "Carroll" }
group.controls.firstName.markDisabled(true);
group.value; // { lastName: "Carroll" }
group.rawValue; // { firstName: "Sandy", lastName: "Carroll" }
FormGroups can have other FormGroups (or FormArrays) as children.
group.setControl(
"addresses",
createFormArray([
createFormGroup({
street: createFormControl(""),
city: createFormControl(""),
state: createFormControl(""),
zip: createFormControl(""),
}),
])
);
group.value;
// {
// addresses: [
// {
// street: "",
// city: "",
// state: "",
// zip: "" ,
// },
// ],
// }
We also have the ability to observe any properties of the FormGroup with Solidjs as well as changes to those properties
For example:
createRenderEffect(() => {
if (!group.children.areValid) {
group.setErrors(null);
return;
}
const firstPartOfEmail = group.value.email.split("@")[0];
if (group.value.name !== firstPartOfEmail) {
group.setErrors({ nameAndEmailMismatch: true });
} else {
group.setErrors(null);
}
});
Here, we're using a render effect to create a custom validator for the FormGroup. This effect is automatically subscribing to some changes under-the-hood (group.children.areValid
, group.value.email
, group.value.name
) and will automatically re-evaluate when these props change. Lets look at what's happening...
You can view the full
IFormGroup
API reference for all of theself
,children
, andchild
properties.
Both FormControls and FormGroups (and FormArrays) have a self
prop which itself is an object containing properties like isValid
, isDisabled
, isTouched
, etc. In a FormControl, control.isValid
is just an alias for control.self.isValid
(same for control.isTouched
, etc). In a FormGroup though, the two properties are different and the self
object contains state local to that FormGroup.
For example, if you do group.markTouched(true)
on the FormGroup, that updates group.self.isTouched
to true but it doesn't affect the state of the FormGroup's children.
group.markTouched(true);
group.isTouched; // true
group.self.isTouched; // true
group.child.isTouched; // false
group.children.areTouched; // false
group.controls.firstName.isTouched; // false
group.controls.lastName.isTouched; // false
Meanwhile, group.isTouched
is actually a getter function equal to group.self.isTouched || group.child.isTouched
. The group.child.isTouched
property is also a memoized getter (its created internally with createMemo()
) which is true if any child is touched (it's false
if there are no children). So a FormGroup is "touched" if either the FormGroup itself has been markTouched(true)
or if any of the FormGroup's children have been markTouched(true)
. But if you just want to see if the FormGroup, itself, has been touched, then you can use group.self.isTouched
. Meanwhile, group.children.areTouched
will return true
if all child controls have been touched. As expected, all of these properties are observable.
group.isTouched; // false
group.self.isTouched; // false
group.child.isTouched; // false
group.children.areTouched; // false
group.controls.firstName.markTouched(true);
group.isTouched; // true
group.self.isTouched; // false
group.child.isTouched; // true
group.children.areTouched; // false
group.controls.lastName.markTouched(true);
group.isTouched; // true
group.self.isTouched; // false
group.child.isTouched; // true
group.children.areTouched; // true
Above, we gave an example that used group.setErrors()
in a way you may have found surprising:
createRenderEffect(() => {
if (!group.children.areValid) {
group.setErrors(null);
return;
}
// ...
});
To the unfamiliar, this might look like we're clearing all errors on the FormGroup if any children are invalid. In reality, group.errors
is a getter function equal to { ...group.children.errors, ...group.self.errors }
or null
if there are no children errors or self errors. Using group.setErrors()
sets self.errors
but it doesn't affect FormGroup children.
You can view the full
IFormGroup
API reference for all of theself
,children
, andchild
properties.
See IFormArray in the API section for all properties and methods.
As you might expect, FormArrays are very similar to FormGroups and are used to group multiple controls together. While a FormGroup groups multiple controls together using an object with named keys, a FormArray groups them together using an array.
FormArrays behave very similarly to FormGroups but with the change that you're dealing with an array of child controls (rather than an object containing child controls). FormArray also has one additional method push()
which adds a new child control to the end of the FormArray. Other than push()
, FormArray and FormGroup have the same interface (which comes from the IAbstractControlContainer
interface). Read the FormGroup section if you haven't yet since all of it also applies to FormArray.
Here's an example using a FormArray (Stackblitz demo):
import { For, type Component } from "solid-js";
import { createFormArray, createFormControl, bindOwner } from "solid-forms";
const ExampleForm: Component<{}> = () => {
const array = createFormArray([createFormControl("")]);
const addPhoneNumber = bindOwner(() => array.push(createFormControl("")));
const removePhoneNumber = () => array.removeControl(array.size - 1);
return (
<form>
<div>
<p>Can you provide us with all of your phone numbers?</p>
</div>
<For each={array.controls}>
{(control, index) => {
return (
<>
<label for={index()}>Phone number {index()}</label>
<input
name={index()}
value={control.value}
oninput={(e) => {
control.setValue(e.currentTarget.value);
}}
onblur={() => control.markTouched(true)}
/>
</>
);
}}
</For>
<button onclick={addPhoneNumber}>Add phone number</button>
<button onclick={removePhoneNumber}>Remove phone number</button>
<button disabled={!array.isValid}>Submit</button>
</form>
);
};
In this example, we have a form asking for all of the user's phone numbers. Users can add or remove phone numbers from the form using buttons in the form (i.e. <button onclick={addPhoneNumber}>Add phone number</button>
). As the user does this, callbacks will file which adds or removes FormControls from the FormArray. Since this is all observable, changes will be reflected in the component.
See the IFormArray API below for more information. Note, all control objects in solid-forms
are immutable so you need to use the provided methods (e.g. IFormArray#push()
) to update their state.
bindOwner()
)Hopefully, given everything you've read up until this point, most of this just makes sense with, I'm guessing, one exception:
import { bindOwner } from "solid-forms";
const ExampleForm: Component<{}> = () => {
const array = createFormArray([createFormControl("")]);
const addPhoneNumber = bindOwner(() => array.push(createFormControl("")));
const removePhoneNumber = () => array.removeControl(array.size - 1);
// ...
};
You may be wondering, what's this bindOwner()
function from "solid-forms" and why doesn't removePhoneNumber()
need it? (though if you're a Solidjs wizz you may have already guessed). Solidjs' reactivity has a concept of an "Owner" that you might never have run into before (the Solidjs "Getting Started" doc currently doesn't mention it and it's not something everyone will run into). The "Owner" is the parent of a given reactive context.
The tl;dr; is that, whenever you have a function that creates a control asyncronously, you need to wrap that function with bindOwner()
. Here we're creating a control inside a button's onclick
handler, which will be called asyncronously, so we need to use bindOwner()
. We don't need to do that for removePhoneNumber
because that function is just removing an already created control from the FormArray (it's not creating a new control).
For a longer (but still summarized) explaination of why this is needed and what an Owner is in Solidjs, here's my attempt at a tl;dr for that (with the caviate that I'm not expert so this is really just my current guess)...
When call a component function (or something else using computations like
createEffect()
), it runs immediately. When it does so, Signals inside that component (orcreateEffect()
, etc), callgetOwner()
and get the current owner of the context. They are able to do this becausegetOwner()
is a global variable. The initial "root" Owner is set by the top levelrender()
function which then calls the top level App component.
The first thing the App component does when it's called is create a new Owner object for itself and set it to the global "owner" variable that is returned by
getOwner()
. Then the App component runs the actual component function you provided and when any Computations inside of it callgetOwner()
(and components are a type of Computation in solid) they receive the Owner associated with the App component. When the App component finishes executing the user provided function (which happens syncronously), then the App component resets the global "owner" viable to whatever it was before it started running. Any components nested inside the App component work the same way. As they are called, they create a new Owner object for themselves and replace the global owner with their own so that nested calls togetOwner()
return this new Owner. When the child component function finishes executing, the original parent owner is restored. Etc.So with this idea, immediately after a component is intialized the component's
getOwner()
context is removed. If you call code asyncronously, for example inside a buttononclick
handler, when that code runs it can't find the parent component's owner anymore. ThebindOwner()
function provided by Solid Forms binds the component's "Owner" context to a function so that, whenever that function is called (even asyncronously) it uses the right owner context. If you create a control asyncronously (e.g. inside of a button onclick handler) then you need to wrap that function withbindOwner()
. ThebindOwner()
function that Solid Forms provides is just a super simple helper utility that looks like this:import { getOwner, runWithOwner } from "solid-js"; export function bindOwner<T>(fn: () => T): () => T { const owner = getOwner(); if (!owner) { throw new Error("No solidjs owner in current context"); } return () => runWithOwner(owner, fn); }
In the future, it seems possible that Solidjs will choose to provide this help directly within the core library.
withControl()
To make it easier to build nice, reusable form components, Solid Forms provides an optional withControl()
higher order component.
For example (Stackblitz demo):
import { Show, type Component } from "solid-js";
import {
createFormGroup,
createFormControl,
withControl,
type IFormGroup,
type IFormControl,
type IFormControlOptions,
} from "solid-forms";
const controlFactory = (props?: { required?: boolean }) => {
const options: IFormControlOptions = props?.required
? { required: true, validators: MyValidators.required }
: undefined;
return createFormGroup({
street: createFormControl("", options),
city: createFormControl("", options),
state: createFormControl("", options),
zip: createFormControl("", options),
});
};
const AddressField = withControl<{ legend?: string }, typeof controlFactory>({
controlFactory,
component: (props) => {
const group = () => props.control;
const controls = () => group().controls;
return (
<fieldset disabled={group().isDisabled}>
<legend>{props.legend || "Your address"}</legend>
<label for="street">Street</label>
<TextInput name="street" control={controls().street} />
<label for="city">City</label>
<TextInput name="city" control={controls().city} />
<label for="state">State</label>
<TextInput name="state" control={controls().state} />
<label for="zip">Zip</label>
<TextInput name="zip" control={controls().zip} />
</fieldset>
);
},
});
We can then reuse this AddressField component in a hypothetical parent component like so (Stackblitz demo):
import { AddressField } from "./AddressField";
export const ParentForm: Component<{}> = () => {
const group = createFormGroup({
firstName: createFormControl(""),
lastName: createFormControl(""),
address: AddressField.control({ required: true }),
});
const controls = () => group.controls;
return (
<form>
<label for="firstName">First name</label>
<TextInput name="firstName" control={controls().firstName} />
<label for="lastName">Last Name</label>
<TextInput name="lastName" control={controls().lastName} />
<AddressField control={controls().address} legend="Your home address" />
</form>
);
};
Lets break this example down.
const controlFactory = (props?: { required?: boolean }) => {
const options: IFormControlOptions = props?.required
? { required: true, validators: MyValidators.required }
: undefined;
return createFormGroup({
street: createFormControl("", options),
city: createFormControl("", options),
state: createFormControl("", options),
zip: createFormControl("", options),
});
};
const AddressField = withControl<{ legend?: string }, typeof controlFactory>({
controlFactory,
component: (props) => {
// ...
},
};
The withControl()
function expects an object with controlFactory
and component
properties. The component property expects a component function. This component function will always receive a control
prop that has the same typescript-type as the control returned by the provided controlFactory
function. The control factory function is responsible for constructing the control used by the component. The control factory function optionally receives the component's properties as arguments. When using the AddressField
, it will have an optional control
property. If you provide that property (like we did in the example above), then the controlFactory
function will never be called. But withControl()
allows us to use our AddressField by itself without providing a control property to it.
For example:
import { AddressField } from "./AddressField";
export const App: Component<{}> = () => {
return (
<div>
<p>These are the questions we will ask you when we need your address.</p>
<AddressField required />
</div>
);
};
This example will work just fine. In this case, withControl()
will see that a control
property wasn't provided and will use the controlFactory function you gave to construct it's control. Since we provided the optional required
prop, that prop will be provided to the controlFactory function in the props param.
The controlFactory function, itself, operates like a component. It is only called once on initialization, and you can choose to use createEffect()
and signals inside of it.
For example:
const controlFactory = (props?: { required?: boolean }) => {
const options: IFormControlOptions = props?.required
? { required: true, validators: MyValidators.required }
: undefined;
const group = createFormGroup({
street: createFormControl("", options),
city: createFormControl("", options),
state: createFormControl("", options),
zip: createFormControl("", options),
});
createEffect(() => {
console.log("current address value", group.value);
});
return group;
};
const AddressField = withControl<{ legend?: string }, typeof controlFactory>({
controlFactory,
component: (props) => {
// ...
},
};
Finally, withControl()
will add a control
property containing your controlFactory
function to the created Solidjs component. You can see this in action in the ParentComponent
example, above
import { AddressField } from "./AddressField";
export const ParentForm: Component<{}> = () => {
const group = createFormGroup({
firstName: createFormControl(""),
lastName: createFormControl(""),
// This use of `AddressField.control` is invoking the AddressField's
// controlFactory function
address: AddressField.control({ required: true }),
});
// ...
};
Also see Stackblitz demo.
const ExampleComponent: Component<{}> = () => {
const control = createFormControl("", {
// This optional validator function is run on every change.
// If we return `null`, there are no errors. Else, we
// can return an object containing errors.
validators: (value: string) =>
value.length === 0 ? { isMissing: true } : null,
});
return (
<div>
<label for="example">Please provide some text</label>
<input
name="example"
type="text"
value={control.value}
oninput={(e) => {
control.setValue(e.currentTarget.value);
}}
onblur={() => control.markTouched(true)}
/>
<Show when={control.isTouched && control.errors?.isMissing}>
<small>Answer required.</small>
</Show>
</div>
);
};
Alternatively, this is effectively the same as the above Stackblitz demo:
const ExampleComponent: Component<{}> = () => {
const control = createFormControl("");
// Under the hood, controls are just Solidjs stores
// so every property is observable. Here we
// observe the `value` prop and set or clear
// errors as it changes
createRenderEffect(() => {
if (control.value.length > 0) {
control.setErrors(null);
} else {
control.setErrors({ isMissing: true });
}
});
return (
<div>
<label for="example">Please provide some text</label>
<input
name="example"
type="text"
value={control.value}
oninput={(e) => {
control.setValue(e.currentTarget.value);
}}
onblur={() => control.markTouched(true)}
/>
<Show when={control.isTouched && control.errors?.isMissing}>
<small>Answer required.</small>
</Show>
</div>
);
};
See Stackblitz demo.
import { Show, type Component } from "solid-js";
import {
createFormGroup,
createFormControl,
type IFormControl,
} from "solid-forms";
const ExampleForm: Component<{}> = () => {
const group = createFormGroup({
name: createFormControl(""),
email: createFormControl(""),
});
const onSubmit = async () => {
if (group.isSubmitted) return;
// do stuff...
// const { name, email } = group.value;
// ...
// group.markSubmitted(true);
};
return (
<form onSubmit={onSubmit}>
<label for="name">Your name</label>
<TextInput name="name" control={group.controls.name} />
<label for="email">Your email address</label>
<TextInput name="email" type="email" control={group.controls.email} />
<button>Submit</button>
</form>
);
};
const TextInput: Component<{
control: IFormControl<string>;
name?: string;
type?: string;
}> = (props) => {
const control = () => props.control;
return (
<>
<input
name={props.name}
type={props.type || "text"}
value={control().value}
oninput={(e) => {
control().markDirty(true);
control().setValue(e.currentTarget.value);
}}
onblur={() => control().markTouched(true)}
disabled={control().isDisabled}
required={control().isRequired}
/>
<Show when={control().isTouched && control().errors?.isMissing}>
<small>Answer required.</small>
</Show>
</>
);
};
Alternatively (Stackblitz demo):
const ExampleForm: Component<{}> = () => {
const group = createFormGroup({
name: createFormControl(""),
email: createFormControl("", {
required: true,
validators: (value: string) =>
!value.includes("@") ? { invalid: true } : null,
}),
});
createRenderEffect(() => {
if (!group.children.areValid) {
group.setErrors(null);
return;
}
const firstPartOfEmail = group.value.email.split("@")[0];
if (firstPartOfEmail !== group.value.name) {
group.setErrors({ invalid: "email must match name" });
} else {
group.setErrors(null);
}
});
return (
<form>
<label for="name">Your name</label>
<TextInput name="name" control={group.controls.name} />
<label for="email">Your email address</label>
<TextInput name="email" type="email" control={group.controls.email} />
<button>Submit</button>
</form>
);
};
Note, all controls objects are immutable unless you use the provided methods to update their state.
interface IAbstractControl<
RawValue = any,
Data extends Record<ControlId, any> = Record<ControlId, any>,
Value = RawValue
> {
/**
* The ID is used to determine where StateChanges originated,
* and to ensure that a given AbstractControl only processes
* values one time.
*/
readonly id: ControlId;
/**
* The data property can store arbitrary custom data. Use the
* `setData` method on `IAbstractControl` to update it.
*
* The `data` property is, itself, an object. You can set individual
* keys on the data property with `setData` but you cannot reset
* or clear the whole object. This is intentional. A library
* maintainer can store private data within the `data` property
* using a symbol without fear of the user accidently erasing it.
*/
readonly data: Data;
/**
* The value of the IAbstractControl.
*
* In an IAbstractControlContainer,
* `value` and `rawValue` can be different, but in a standard
* `IAbstractControl` `value` is just an alias for `rawValue`.
* See the IAbstractControlContainer interface for possible differences
* between `value` and `rawValue`.
*/
readonly value: Value;
/**
* The value of the IAbstractControl.
*
* In an IAbstractControlContainer,
* `value` and `rawValue` can be different, but in a standard
* `IAbstractControl` `value` is just an alias for `rawValue` and
* rawValue just contains the control's value.
* See the IAbstractControlContainer interface for possible differences
* between `value` and `rawValue`.
*/
readonly rawValue: RawValue;
/**
* `true` if this control is disabled, false otherwise.
* This is an alias for `self.isDisabled`.
*/
readonly isDisabled: boolean;
/**
* `true` if this control is touched, false otherwise.
* This is an alias for `self.isTouched`.
*/
readonly isTouched: boolean;
/**
* `true` if this control is dirty, false otherwise.
* This is an alias for `self.isDirty`.
*/
readonly isDirty: boolean;
/**
* `true` if this control is readonly, false otherwise.
* This is an alias for `self.isReadonly`.
*/
readonly isReadonly: boolean;
/**
* `true` if this control is submitted, false otherwise.
* This is an alias for `self.isSubmitted`.
*/
readonly isSubmitted: boolean;
/**
* `true` if this control is required, false otherwise.
* This is an alias for `self.isRequired`.
*
* Note that this property doesn't
* have any predefined meaning for IAbstractControls and it doesn't affect
* validation in any way. It is up to you to decide what meaning, if any,
* to give to this property and how to use it. For example, if you
* validated the control inside a `createEffect()`, you could choose to alter the
* validation based on whether the control was marked as `required` or
* not.
*/
readonly isRequired: boolean;
/**
* Contains a `ValidationErrors` object if this control
* has any errors. Otherwise contains `null`.
*
* An alias for `self.errors`.
*/
readonly errors: ValidationErrors | null;
/**
* A validator function that is run on rawValue changes and which
* generates errors associated with the source "CONTROL_DEFAULT_SOURCE".
*/
readonly validator: ValidatorFn | null;
/**
* `true` if this control is pending, false otherwise.
* This is an alias for `self.isPending`.
*/
readonly isPending: boolean;
/**
* Valid if `errors === null && !isPending`
*
* This is an alias for `self.valid`.
*/
readonly isValid: boolean;
/**
* The `self` object on an abstract control contains
* properties reflecting the control's personal state. On an
* IAbstractControlContainer, the personal state can differ
* from the control's state. For example, an
* IAbstractControlContainer will register as disabled if
* the control itself has been marked as disabled OR if
* all of it's child controls are disabled.
*
* Marking the control container
* itself as disabled doesn't mark the container's children as
* disabled. On a standard IAbstractControl though,
* the "self" properties are the same as regular properties.
* I.e. `self.isInvalid` is the same as `isInvalid` on a
* standard IAbstractControl (actually, `isInvalid` is
* an alias for `self.isInvalid` on a standard control).
*/
readonly self: {
/** `this.self.errors === null && !this.self.isPending` */
readonly isValid: boolean;
/** `true` if this control is disabled, false otherwise. */
readonly isDisabled: boolean;
/** `true` if this control is touched, false otherwise. */
readonly isTouched: boolean;
/**
* `true` if this control is dirty, false otherwise.
*
* Dirty can be thought of as, "Has the value changed?"
* Though the isDirty property must be manually set by
* the user (using `markDirty()`) and is not automatically
* updated.
*/
readonly isDirty: boolean;
/**
* `true` if this control is readonly, false otherwise.
*
* This property does not have any predefined meeting for
* an IAbstractControl. You can decide if you want to give
* it meaning by, for example, using this value to set
* an input's readonly status (e.g.
* `<input readonly={control.isReadonly} />`)
*/
readonly isReadonly: boolean;
/** `true` if this control is submitted, false otherwise. */
readonly isSubmitted: boolean;
/**
* `true` if this control is required, false otherwise.
*
* Note that this property doesn't
* have any predefined meaning for IAbstractControls and it doesn't affect
* validation in any way. It is up to you to decide what meaning, if any,
* to give to this property and how to use it. For example, if you
* validated the control inside a `createEffect()` you could alter the
* validation based on whether the control was marked as `required` or
* not.
*/
readonly isRequired: boolean;
/** `true` if this control is pending, false otherwise. */
readonly isPending: boolean;
/**
* Contains a `ValidationErrors` object if this control
* has any errors. Otherwise contains `null`.
*/
readonly errors: ValidationErrors | null;
/**
* *More advanced-ish*
*
* Contains a map of ControlId values and ValidationErrors.
* The errorsStore allows partitioning errors so that
* they can be associated with different sources and so
* that one source does not overwrite another source.
*
* The `self.errors` property gets its errors from the errorsStore.
*/
readonly errorsStore: ReadonlyMap<ControlId, ValidationErrors>;
/**
* More advanced-ish*
*
* A set of ControlIds. `self.isPending` is true so long
* as `pendingStore.size > 0`. Because this is a set, you
* can track multiple pending "things" at once. This
* control will register as pending until all of the "things"
* have resolved. Use the `markPending()` method with
* the `source` option to update the pendingStore.
*/
readonly pendingStore: ReadonlySet<ControlId>;
/**
* More advanced-ish*
*
* A map of ControlIds and ValidatorFns. The `validator`
* property is composed of all the validator functions in the
* `validatorStore`. The validatorStore allows you to change
* individual validator functions on the control without
* affecting other validator functions on the control.
*
* When you use the `setValidators` method, you are updating
* the validatorStore.
*/
readonly validatorStore: ReadonlyMap<ControlId, ValidatorFn>;
};
/**
* If this control is disabled, the status is `"DISABLED"`,
* else if this control is pending, the status is `"PENDING"`,
* else if this control has any errors, the status is `"INVALID"`,
* else the status is `"VALID"`.
*/
readonly status: "DISABLED" | "PENDING" | "INVALID" | "VALID";
[AbstractControlInterface]: true;
/** set the control's value */
setValue(value: RawValue): void;
/**
* If provided a `ValidationErrors` object or `null`, replaces `self.errors`.
* Optionally, provide a source ID and the change will be partitioned
* assocaited with the source ID. The default source ID is
* "CONTROL_DEFAULT_SOURCE".
*
* If you provide a `Map` object containing `ValidationErrors` keyed to source IDs,
* that will replace the `self.errorsStore` associated with this control.
*/
setErrors(
value: ValidationErrors | null | ReadonlyMap<ControlId, ValidationErrors>,
options?: { source?: ControlId }
): void;
/**
* If you provide a `ValidationErrors` object, that object is merged with the
* existing errors associated with the source ID. If the error object has
* keys equal to `null`, errors associated with those keys are deleted
* from the errors object.
*
* If you provide a `Map` object containing `ValidationErrors` keyed to source IDs,
* that object is merged with the existing `errorsStore`.
*/
patchErrors(
value: ValidationErrors | ReadonlyMap<ControlId, ValidationErrors>,
options?: { source?: ControlId }
): void;
/** sets `self.isTouched` */
markTouched(value: boolean): void;
/** sets `self.isDirty` */
markDirty(value: boolean): void;
/** sets `self.isReadonly` */
markReadonly(value: boolean): void;
/**
* Sets `self.isRequired`.
*
* Note that this property doesn't
* have any predefined meaning for IAbstractControls and it doesn't affect
* validation in any way. It is up to you to decide what meaning, if any,
* to give to this property and how to use it. For example, if you
* validated the control inside a `createEffect()` you could alter the
* validation based on whether the control was marked as `required` or
* not.
*/
markRequired(value: boolean): void;
/**
* Set `self.isDisabled`.
*
* Note that `self.isDisabled`` affect's the control's `status`
* property. Additionally, `IAbstractControlContainer's` ignore
* disabled children in many cases. For example, the `value` of a
* control container is equal to the value of it's _enabled_ children
* (if you want to see the value including disabled children, use
* `rawValue`).
*/
markDisabled(value: boolean): void;
/** sets `self.isSubmitted` */
markSubmitted(value: boolean): void;
/** sets `self.pendingStore` and `self.isPending` */
markPending(
value: boolean | ReadonlySet<ControlId>,
options?: { source?: ControlId }
): void;
/** sets `validator` and `self.validatorStore` */
setValidators(
value:
| ValidatorFn
| ValidatorFn[]
| ReadonlyMap<ControlId, ValidatorFn>
| null,
options?: { source?: ControlId }
): void;
/**
* The data property can store arbitrary custom data. Use the
* `setData` method on `IAbstractControl` to update it.
*
* The `data` property is, itself, an object. You can set individual
* keys on the data property with `setData` but you cannot reset
* or clear the whole object. This is intentional. A library
* maintainer can store private data within the `data` property
* using a symbol without fear of the user accidently erasing it.
*/
setData<K extends keyof Data>(key: K, data: Data[K]): void;
}
export interface IAbstractControlContainer<
Controls extends GenericControlsObject = any,
Data = any
> extends IAbstractControl<
ControlsRawValue<Controls>,
Data,
ControlsValue<Controls>
> {
/** Child controls associated with this container */
readonly controls: Controls;
/** The number of controls associated with this container */
readonly size: number;
/** Only returns values for enabled child controls. */
readonly value: ControlsValue<Controls>;
/**
* Returns values for both enabled and disabled child controls.
*/
readonly rawValue: ControlsRawValue<Controls>;
/** Will return true if `this.self.isValid` and `this.children.areValid` */
readonly isValid: boolean;
/** Will return true if `this.self.isDisabled` or `this.children.areDisabled` */
readonly isDisabled: boolean;
/** Will return true if `this.self.isReadonly` or `this.children.areReadonly` */
readonly isReadonly: boolean;
/** Will return true if `this.self.isRequired` or `this.child.isRequired` */
readonly isRequired: boolean;
/** Will return true if `this.self.isPending` or `this.child.isPending` */
readonly isPending: boolean;
/** Will return true if `this.self.isTouched` or `this.child.isTouched` */
readonly isTouched: boolean;
/** Will return true if `this.self.isDirty` or `this.child.isDirty` */
readonly isDirty: boolean;
/** Will return true if `this.self.isSubmitted` or `this.children.areSubmitted` */
readonly isSubmitted: boolean;
/** Contains `{ ...this.children.errors, ...this.self.errors }` or `null` if there are none */
readonly errors: ValidationErrors | null;
readonly child: {
/** Will return true if *any* `enabled` direct child control is `valid` */
readonly isValid: boolean;
/** Will return true if *any* direct child control is `disabled` */
readonly isDisabled: boolean;
/** Will return true if *any* `enabled` direct child control is `readonly` */
readonly isReadonly: boolean;
/** Will return true if *any* `enabled` direct child control is `required` */
readonly isRequired: boolean;
/** Will return true if *any* `enabled` direct child control is `pending` */
readonly isPending: boolean;
/** Will return true if *any* `enabled` direct child control is `touched` */
readonly isTouched: boolean;
/** Will return true if *any* `enabled` direct child control is `dirty` */
readonly isDirty: boolean;
/** Will return true if *any* `enabled` direct child control is `submitted` */
readonly isSubmitted: boolean;
};
readonly children: {
/** Will return true if *all* `enabled` direct child control's are `valid` */
readonly areValid: boolean;
/** Will return true if *all* direct child control's are `disabled` */
readonly areDisabled: boolean;
/** Will return true if *all* `enabled` direct child control's are `readonly` */
readonly areReadonly: boolean;
/** Will return true if *all* `enabled` direct child control's are `required` */
readonly areRequired: boolean;
/** Will return true if *all* `enabled` direct child control's are `pending` */
readonly arePending: boolean;
/** Will return true if *all* `enabled` direct child control's are `touched` */
readonly areTouched: boolean;
/** Will return true if *all* `enabled` direct child control's are `dirty` */
readonly areDirty: boolean;
/** Will return true if *all* `enabled` direct child control's are `submitted` */
readonly areSubmitted: boolean;
/** Contains *all* `enabled` child control errors or `null` if there are none */
readonly errors: ValidationErrors | null;
/**
* Mark all direct children as disabled. Use the `deep: true`
* option to instead mark all direct and indirect children
* as disabled.
*/
markDisabled(value: boolean, options?: { deep?: boolean }): void;
/**
* Mark all direct children as touched. Use the `deep: true`
* option to instead mark all direct and indirect children
* as touched.
*/
markTouched(value: boolean, options?: { deep?: boolean }): void;
/**
* Mark all direct children as dirty. Use the `deep: true`
* option to instead mark all direct and indirect children
* as dirty.
*/
markDirty(value: boolean, options?: { deep?: boolean }): void;
/**
* Mark all direct children as readonly. Use the `deep: true`
* option to instead mark all direct and indirect children
* as readonly.
*/
markReadonly(value: boolean, options?: { deep?: boolean }): void;
/**
* Mark all direct children as required. Use the `deep: true`
* option to instead mark all direct and indirect children
* as required.
*/
markRequired(value: boolean, options?: { deep?: boolean }): void;
/**
* Mark all direct children as submitted. Use the `deep: true`
* option to instead mark all direct and indirect children
* as submitted.
*/
markSubmitted(value: boolean, options?: { deep?: boolean }): void;
/**
* Mark all direct children as pending. Use the `deep: true`
* option to instead mark all direct and indirect children
* as pending.
*/
markPending(
value: boolean,
options?: { source?: ControlId; deep?: boolean }
): void;
};
[AbstractControlContainerInterface]: true;
/**
* Apply a partial update to the values of some children but
* not all.
*/
patchValue(value: unknown): void;
/** sets the `controls` property */
setControls(controls: Controls): void;
/** stores the provided control in `controls[key]` */
setControl(key: unknown, control: unknown): void;
/**
* If provided a control value, removes the given control from
* `controls`. If provided a control key value, removes the
* control associated with the given key from `controls`.
*/
removeControl(key: unknown): void;
}
See the IAbstractControl
interface, above. IFormControl has the same properties as that interface. Like other solid-forms
controls, form controls immutable unless you use the provided methods to update it's state.
interface IFormControl<
Value = any,
Data extends Record<ControlId, any> = Record<ControlId, any>
> extends IAbstractControl<Value, Data, Value> {
[FormControlInterface]: true;
}
createFormControl()
Use the createFormControl()
function to create a new Solidjs store conforming to the IFormControl
interface.
function createFormControl<
Value,
Data extends Record<ControlId, any> = Record<ControlId, any>
>(
value?: Value,
options?: IFormControlOptions<Data>
): IFormControl<Value, Data>;
interface IFormControlOptions<
Data extends Record<ControlId, any> = Record<ControlId, any>
> {
id?: ControlId;
data?: Data;
disabled?: boolean;
touched?: boolean;
dirty?: boolean;
readonly?: boolean;
required?: boolean;
submitted?: boolean;
errors?: null | ValidationErrors | ReadonlyMap<ControlId, ValidationErrors>;
validators?:
| null
| ValidatorFn
| ValidatorFn[]
| ReadonlyMap<ControlId, ValidatorFn>;
pending?: boolean | ReadonlySet<ControlId>;
}
See the IAbstractControlContainer
interface, above. IFormGroup has the same properties as that interface. Like other solid-forms
controls, form groups immutable unless you use the provided methods to update it's state.
interface IFormGroup<
Controls extends { [key: string]: IAbstractControl } = {
[key: string]: IAbstractControl;
},
Data extends Record<ControlId, any> = Record<ControlId, any>
> extends IAbstractControlContainer<Controls, Data> {
[FormGroupInterface]: true;
}
createFormGroup()
Use the createFormGroup()
function to create a new Solidjs store conforming to the IFormGroup
interface.
function createFormGroup<
Controls extends { [key: string]: IAbstractControl } = {
[key: string]: IAbstractControl;
},
Data extends Record<ControlId, any> = Record<ControlId, any>
>(
controls?: Controls,
options?: IFormGroupOptions<Data>
): IFormGroup<Controls, Data>;
interface IFormGroupOptions<
Data extends Record<ControlId, any> = Record<ControlId, any>
> {
id?: ControlId;
data?: Data;
disabled?: boolean;
touched?: boolean;
dirty?: boolean;
readonly?: boolean;
required?: boolean;
submitted?: boolean;
errors?: null | ValidationErrors | ReadonlyMap<ControlId, ValidationErrors>;
validators?:
| null
| ValidatorFn
| ValidatorFn[]
| ReadonlyMap<ControlId, ValidatorFn>;
pending?: boolean | ReadonlySet<ControlId>;
}
See the IAbstractControlContainer
interface, above. IFormArray has the same properties as that interface with one addtion: push()
for adding new child controls to the end of the form array.Like other solid-forms
controls, form arrays immutable unless you use the provided methods to update it's state.
interface IFormArray<
Controls extends ReadonlyArray<IAbstractControl> = ReadonlyArray<IAbstractControl>,
Data extends Record<ControlId, any> = Record<ControlId, any>
> extends IAbstractControlContainer<Controls, Data> {
[FormArrayInterface]: true;
push(control: Controls[number]): void;
}
createFormArray()
Use the createFormArray()
function to create a new Solidjs store conforming to the IFormArray
interface.
function createFormArray<
Controls extends ReadonlyArray<IAbstractControl> = ReadonlyArray<IAbstractControl>,
Data extends Record<ControlId, any> = Record<ControlId, any>
>(
controls?: Controls,
options?: IFormArrayOptions<Data>
): IFormArray<Controls, Data>;
interface IFormArrayOptions<
Data extends Record<ControlId, any> = Record<ControlId, any>
> {
id?: ControlId;
data?: Data;
disabled?: boolean;
touched?: boolean;
dirty?: boolean;
readonly?: boolean;
required?: boolean;
submitted?: boolean;
errors?: null | ValidationErrors | ReadonlyMap<ControlId, ValidationErrors>;
validators?:
| null
| ValidatorFn
| ValidatorFn[]
| ReadonlyMap<ControlId, ValidatorFn>;
pending?: boolean | ReadonlySet<ControlId>;
}
withControl()
A higher order component function for creating reusable form components.
import { withControl } from "solid-forms";
function withControl<
Props extends {},
ControlFactory extends (...args: [any, ...any[]]) => IAbstractControl
>(
options: IWithControlOptions<Props, ControlFactory>
): WithControlReturnType<Props, ControlFactory>;
interface IWithControlOptions<
Props extends {},
ControlFactory extends (...args: [any, ...any[]]) => IAbstractControl
> {
controlFactory: ControlFactory;
component: Component<
WithControlProps<Props, ControlFactory> & {
control: ReturnType<ControlFactory>;
}
>;
}
type WithControlReturnType<
Props extends {},
ControlFactory extends (...args: [any, ...any[]]) => IAbstractControl
> = ((
props: WithControlProps<Props, ControlFactory> & {
control?: ReturnType<ControlFactory>;
}
) => JSX.Element) & {
/**
* Factory function to build the component's default form control.
* Note, you can pass any form control to the component which
* satisfies the component's interface. You do not need to use
* this factory function.
*
* Example usage:
* ```ts
* const TextInput = withControl({
* // etc...
* });
*
* createFormGroup({
* street: TextInput.control(),
* city: TextInput.control(),
* state: TextInput.control(),
* zip: TextInput.control(),
* })
* ```
*/
control: ControlFactory;
};
bindOwner()
Helper to bind the owner of the current context to the supplied function.
import { bindOwner } from "solid-forms";
function bindOwner<T>(fn: () => T): () => T;
isAbstractControl()
Check's if an object is an IAbstractControl
.
import { isAbstractControl } from "solid-forms";
function isAbstractControl(object?: unknown): object is IAbstractControl;
isAbstractControlContainer()
Check's if an object is an IAbstractControlContainer
.
import { isAbstractControlContainer } from "solid-forms";
function isAbstractControlContainer(
object?: unknown
): object is IAbstractControlContainer;
isFormControl()
Check's if an object is an IFormControl
.
import { isFormControl } from "solid-forms";
function isFormControl(object?: unknown): object is IFormControl;
isFormGroup()
Check's if an object is an IFormGroup
.
import { isFormGroup } from "solid-forms";
function isFormGroup(object?: unknown): object is IFormGroup;
isFormArray()
Check's if an object is an IFormArray
.
import { isFormArray } from "solid-forms";
function isFormArray(object?: unknown): object is IFormArray;
This library was created by John Carroll with significant inspiration from Angular's ReactiveFormsModule
.
Author: jorroll
Source code: https://github.com/jorroll/solid-form
License: Unlicense license
#react #typescript #javascript #solid