docs/development/style-guide/frontend/README.md
OpenProject follows the AirBnB's style guide regarding to the code format.
OpenProject follows the Angular's style guide patterns.
Declarative Programming is a paradigm where the code describes what to do by encapsulating the how to do it (implementation details) under abstractions. The ultimate result of declarative programming is the creation of a new Domain Specific Language (DSL).
Encapsulate logic in methods with meaningful names.
// Imperative programming
const bestProducts = [];
for(let i = 0; i < products.length; i++) {
let product = products[i];
if (product.rating >= 5 && product.price < 100) {
bestProducts.push(product);
}
}
// More declarative
const bestProducts = products.filter(function(product) {
return product.rating >= 5 && product.price < 100;
});
// Most declarative, implementation details are hidden in a function
const bestProducts = getBestProducts();
An example in OpenProject would be the APIV3Service that encapsulates all the logic to deal with the OpenProject API.
Not capable of or susceptible to change. An immutable value can’t be changed, when edited a new copy is returned.
Do not mutate objects, spread the word.
const copy = {...originalObject};
const add = {...originalObject, propertyToChange: 'new value'};
const remove = {propertyToDelete, ...newObjectWithoutThePropertyToDelete};
clone = x => [...x];
push = y => x => [...x, y];
pop = x => x.slice(0, -1);
unshift = y => x => [y, ...x];
shift = x => x.slice(1);
sort = f => x => [...x].sort(f);
delete = i => x => [...x.slice(0, i), ...x.slice(i + 1)];
splice = (s, c, ...y) => x => [...x.slice(0, s), ...y, ...x.slice(s + c)];
The app has a single way to read and to write the state, and both are separated (Command Query Segregation).
We can differentiate two types of states in our application:
There are also two types of other state that our frontend application has to be concerned with:
For the server, most of the time this means the database contents. Keeping our local state up-to-date with this is a hard problem to solve, and will force us to make unwanted API calls or unprovable assertions. Long-term, we would like to have live (e.g. websocket) connections for pieces of state, so we can always be up-to-date with the database without bombarding the server in requests.
Syncing with remote clients' state is currently nonexistent. Clients submit their updates only to the server. When submitting, clients submit a version token of the resource, which gets updated by the server. If a client tries to work on an older version of the resource, the request will fail. Usually is no sophisticated method in place to handle these; an error toaster will be thrown. Direct state transfer or sharing between clients is currently not done. Since this is a very hard problem to solve, there are no plans to change this significantly.
State relating to the component's view (or children) should be declared and managed in that component. Children that rely on this state should receive it via inputs and request changes via outputs. Sometimes, this tree might prove too complex to easily hand local state and events up and down via this mechanism. In that case you may create an Akita store that is injected into the first shared parent component, and manage the state there. You must not save global state in a local component or service. You should not save state in non-akita services. The goal is to have a unified, observable-based interface to all application state.
Most of our backend-related data is in the entity format. To capture this, there must be a global entity store. An example implementation is the in-app-notification store. This store olds a reference of all entities of a particular type that are in-use somewhere in the application as well as a list of IDs for entity collections of that type. Stores and components consuming a particular entity type must go through the global entity store to perform CRUD operations, so that updates can be properly reflected across the application.
Mutable operations on the entities can have side effects on different collections and entities currently in use by other parts of the application. Oftentimes, the frontend cannot know beforehand which operations will have what kind of impact. This means that the respective collections and entities have to be refreshed from the backend. Some examples:
For this use-case, we have implemented a global actions service. You can dispatch actions here, and other parts of the application can listen to these actions. Think of it like a global event bus. These actions are typed.
To reduce server requests, side effects should be be calculated in the frontend. If this is impossible, the updating store must send out a global event to notify other parts that the specific event occurred.
Note: The proper solution to this problem would be a backend that can push updates for collections and entities that we are requiring. However, implementing and relying on websockets comes with its own challenges.
Angular also follows the unidirectional data flow pattern in the view to improve the performance and simplify the state distribution:
Mental mindset to build clearer apps based on the differentiation between Container Components (CC), Presentational Components (PC) and Services.
Are state and logic containers.
Represent a feature that interacts with the state. This could be a page (routed component) but also standalone components (e.g. sign-in button (tied to the AuthService)).
Pages, components that are routed, are usually container components since the need to fetch state to display it.
Concentrate the interaction with the state (Stores) in Container Components.
Are the building blocks of the UI.
Components from UI libraries are usually Presentational Components (e.g., material button).
Clean code is easily readable, understandable, changeable, extensible, scalable and maintainable.
Do follow BEM directives: