This is an angular application. It doesn't run standalone but requires the built code from shared at build time and a working backend at runtime.
Please look in the root readme for general information about how to start and develop the whole application.
You can either use the Angular CLI:
Run ng generate component component-name
to generate a new component. You can also use ng generate directive|pipe|service|guard|module|web-worker
.
Or an extension: Angular Schematics (vscode).
You can build the application with different application environments.
Keep in mind that it is also possible to add a proxy during development if you want to change e.g. the backend URL.
- We are using Bootstrap as a style framework.
- Where should I put my CSS? (Guideline)
- Use Bootstrap classes whenever possible (look at the utility classes).
- If you can reuse the styling, add a class to the style.scss (or the components SCSS if it's only used there).
- If the styling is custom and only applicable to one specific element, use inline CSS.
- If the inline styling is very big, complicated, or makes use of any SCSS or advanced CSS not applicable to inline styles, put it into the components SCSS file.
- See also Bootstraps best practices.
-
Order of attributes in the Angular templates
- As long as eslint doesn't have a lint rule for it it is just encouraged to use the following order of attributes.
- Structural directive (e.g.
*ngIf
) - Template reference (e.g.
#myId
) - Component inputs (e.g.
[myInput]
) - Two way binding/banana in the box (e.g.
[(myBanana)]
) - Outputs (e.g.
(onChange)
) - Directives (e.g.
[myDirective]
,[class.my_class]
,[ngStyle]
,[attr.myAttr]
) class
attributestyle
attribute- Other attributes (e.g.
href
,type
)
- Bootstrap - A styling framework
- Bootstrap-icons - the icons we use in the application
- List of all icons
- We import the
.css
. Use it like<i class="bi-alarm"></i>
- ngBootstrap - Angular components for bootstrap
- openlayers - A library for displaying maps
- lodash-es - A utility library for common JS tasks
- rxjs - A library for reactive programming (= observer pattern on steroids)
- NGRX - A library for state management
For most, the frontend follows the project file structure dictated by angular.
We call Angular components, pipes, directives, services, and modules "Angular elements".
In src/app/state are all actions, reducers, selectors and state that are used by NGRX.
In src/app
and every descending folder, the following guidelines apply:
/core
:- Singleton-services and route guards that can be used by all other Angular elements that are direct or indirect children of the
core
's parent-folder
- Singleton-services and route guards that can be used by all other Angular elements that are direct or indirect children of the
/shared
:- Utility Angular elements as well as classes, functions, types, etc. that (can) have multiple instances and can be used by all other Angular elements that are direct or indirect children of the
shared
's parent-folder - Every folder with Angular components, pipes, or directives in a
shared
-folder should have its own module, that exports these Angular elements
- Utility Angular elements as well as classes, functions, types, etc. that (can) have multiple instances and can be used by all other Angular elements that are direct or indirect children of the
/features
- Components/pipes/directives that should only be used in the
/pages
-folder at the same level (in opposite to/shared
no nested folders)
- Components/pipes/directives that should only be used in the
/pages
:- All Angular elements and utilities that are only used according to the route (-> lazy loading)
Commonly used selectors should go in ./src/app/state/application/selectors.
You can assume that the Store has the current exercise state (either of a live exercise or an exercise in time travel) if you are in src/app/pages/exercises/exercise
. We use route guards for this.
If you want to modify the exercise state, do not do it via reducers in the store, but propose an action (optimistically) via the ExerciseService. The action will automatically be applied to the store.
If you want to switch between time travel, live exercise and no exercise (e.g. on the landing page), use the ApplicationService.
By default, we don't use ChangeDetectionStrategy.OnPush
because it complicates the code and increases the skill level needed to work with the code while providing a mostly negligible performance benefit.
For the same reason, there are also no optimizations made regarding how often the change detection is triggered by zones. E.g. OpenLayers runs inside the zone and therefore triggers the change detection on every pointermove
- and requestAnimationFrame
-event (which are also patched by zone.js). Read the angular documentation regarding change detection for more information.
You can find the exercise map in src/app/pages/exercises/exercise/shared/exercise-map.
element
: The data that represents aMaterial
,Personnel
,Vehicle
, etc. that is saved in the state.feature
: The OpenLayers feature representing an element and is rendered on the map canvas.
The ExerciseMapComponent is the Angular component that provides the canvas on which the map should be rendered.
The OlMapManager manages all the OpenLayers stuff and renders the map on the canvas. The map consists of different layers. Each layer only displays one kind of element. How an element in this layer should be rendered and what interactions are possible is defined in the specific ElementFeatureManagers. The feature managers for features that should be moveable by the user extend MoveableFeatureManager, which is the central point for all movement logic. They have a custom API that allows reacting to changes in an element (ElementManager) and an API that allows for interaction with other elements via the OlMapManager (FeatureManager).
As described in the root README.md, we use actions to propose changes. Such actions can be proposed optimistically. Note that the described synchronization mechanisms only make sure that the states between the clients and the server are in sync. In addition, it must be guaranteed that the UI is always in sync with the current state in the store. While Angular deals with this, for the most part, the OpenLayers implementation doesn't do this by default. Therefore, desynchronization is possible when, e.g., dragging an element. To fix this, the respective proposals should be optimistic. See #298 in this context.