mapcomplete/Docs/Architecture.md
2023-10-27 00:05:34 +03:00

6.7 KiB

Architecture

This document aims to give an architectural overview of how MapComplete is built. It should give some feeling on how everything fits together.

Servers?

There are no servers for MapComplete, all services are configured by third parties.

Minimal HTML - Minimal CSS

There is quasi no HTML. Most of the components are generated by TypeScript and attached dynamically. The HTML is a barebones skeleton which serves every theme.

The UIEventSource

Most (but not all) objects in MapComplete get all the state they need as a parameter in the constructor. However, as is the case with most graphical applications, there are quite some dynamical values.

All values which change regularly are wrapped into a UIEventSource. A UIEventSource is a wrapper containing a value and offers the possibility to add a callback function which is called every time the value is changed (with setData)

Furthermore, there are various helper functions, the most widely used one being map - generating a new event source with the new value applied. Note that map will also absorb some changes, e.g. const someEventSource : UIEventSource<string[]> = ... ; someEventSource.map(list = list.length) will only trigger when the length of the list has changed.

An object which receives a UIEventSource is responsible of responding to changes of this object. This is especially true for UI-components.

UI


export default class MyComponent {

    constructor(neededParameters, neededUIEventSources) {
    
    }

}

The Graphical User Interface is composed of various UI-elements. For every UI-element, there is a BaseUIElement which creates the actual HTMLElement when needed.

There are some basic elements, such as:

  • FixedUIElement which shows a fixed, unchangeable element
  • Img to show an image
  • Combine which wraps everything given (strings and other elements) in a div
  • List

There is one special component: the VariableUIElement The VariableUIElement takes a UIEventSource<string|BaseUIElement> and will dynamically show whatever the UIEventSource contains at the moment.

For example:


const src : UIEventSource<string> = ... // E.g. user input, data that will be updated... new VariableUIElement(src)
.AttachTo('some-id') // attach it to the html

Note that every component offers support for onClick( someCallBack)

Translations

To add a translation:

  1. Open langs/en.json
  2. Find a correct spot for your translation in the tree
  3. run npm run generate:translations
  4. import Translations
  5. Translations.t.<your-translation>.Clone() is the UIElement offering your translation

Input elements

Input elements are a special kind of BaseElement which offer a piece of a form to the user, e.g. a TextField, a Radio button, a dropdown, ...

The constructor will ask all the parameters to configure them. The actual value can be obtained via inputElement.GetValue(), which is a UIEventSource that will be triggered every time the user changes the input.

Advanced elements

There are some components which offer useful functionality:

  • The subtleButton which is a friendly, big button
  • The Toggle: const t = new Toggle( componentA, componentB, source) is a UIEventSource which shows componentA as long as source contains true and will show componentB otherwise.

Styling

Styling is done as much as possible with TailwindCSS. It contains a ton of utility classes, each of them containing a few rules.

For example: someBaseUIElement.SetClass("flex flex-col border border-black rounded-full") will set the component to be a flex object, as column, with a black border and pill-shaped.

If Tailwind is not enough, use baseUiElement.SetStyle("background: red; someOtherCssRule: abc;").

An example

For example: the user should input whether or not a shop is closed during public holidays. There are three options:

  1. closed
  2. opened as usual
  3. opened with different hours as usual

In the case of different hours, input hours should be too.

This can be constructed as following:


    // We construct the dropdown element with values and labelshttps://tailwindcss.com/
    const isOpened = new Dropdown<string>(Translations.t.is_this_shop_opened_during_holidays,
        [
         { value: "closed", Translation.t.shop_closed_during_holidays.Clone()},
         { value: "open", Translations.t.shop_opened_as_usual.Clone()},
         { value: "hours", Translations.t.shop_opened_with_other_hours.Clone()}
        ] )
        
     const startHour = new DateInput(...)drop
     const endHour = new DateInput( ... )   
     // We construct a toggle which'll only show the extra questions if needed
     const extraQuestion = new Toggle(
            new Combine([Translations.t.openFrom, startHour, Translations.t.openTill, endHour]),
            undefined,
            isOpened.GetValue().map(isopened => isopened === "hours")
         )

    return new Combine([isOpened, extraQuestion])

Constructing a special class

If you make a specialized class to offer a certain functionality, you can organize it as following:

  1. Create a new class:

export default class MyComponent {

    constructor(neededParameters, neededUIEventSources) {
    
    }

}

  1. Construct the needed UI in the constructor

export default class MyComponent {

    constructor(neededParameters, neededUIEventSources) {
    
    
            const component = ...
            const toggle = ...
            ... other components ...
            
            toggle.GetValue.AddCallbackAndRun(isSelected => { .. some actions ... }
            
            new Combine([everything, ...] )
    }

}

  1. You'll notice that you'll end up with one certain component (in this example the combine) to wrap it all together. Change the class to extend this type of component and use super() to wrap it all up:

export default class MyComponent extends Combine {

    constructor(...) {
    
        ...
        super([everything, ...])
    
    }

}

Assets

Themes

Theme and layer configuration files go into assets/layers and assets/themes.

Images

Other files (mostly images that are part of the core of MapComplete) go into assets/svg and are usable with Svg.image_file_ui(). Run npm run generate:images if you added a new image.

Logic

The last part is the business logic of the application, found in the directory Logic. Actors are small objects which react to UIEventSources to update other eventSources.

State.state is a big singleton object containing a lot of the state of the entire application. That one is a bit of a mess.