Skip to content

Commit

Permalink
WIP finished: Migrated to Vue 3, Bootstrap 5, ESM, Vite (only minor i…
Browse files Browse the repository at this point in the history
…ssues remaining)
  • Loading branch information
cdauth committed Nov 14, 2023
1 parent 44c16a2 commit f4555ee
Show file tree
Hide file tree
Showing 66 changed files with 603 additions and 696 deletions.
4 changes: 2 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM node:15.12-alpine
FROM node:21-alpine
MAINTAINER Candid Dauth <[email protected]>

CMD yarn run server
Expand All @@ -21,6 +21,6 @@ RUN cd .. && yarn install
RUN cd .. && yarn run build

USER root
RUN chown -R root:root /opt/facilmap && chown -R facilmap:facilmap /opt/facilmap/server/node_modules/.cache
RUN chown -R root:root /opt/facilmap && mkdir -p /opt/facilmap/server/node_modules/.cache && chown -R facilmap:facilmap /opt/facilmap/server/node_modules/.cache

USER facilmap
2 changes: 0 additions & 2 deletions client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,7 @@
},
"devDependencies": {
"@types/geojson": "^7946.0.13",
"@types/rollup-plugin-auto-external": "^2.0.5",
"rimraf": "^5.0.5",
"rollup-plugin-auto-external": "^2.0.0",
"typescript": "^5.2.2",
"vite": "^4.5.0",
"vite-plugin-dts": "^3.6.3"
Expand Down
1 change: 1 addition & 0 deletions client/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -369,6 +369,7 @@ export default class Client {

async createPad(data: PadData<CRU.CREATE>): Promise<void> {
const obj = await this._emit("createPad", data);
this._set(this.state, 'serverError', undefined);
this._set(this.state, 'readonly', false);
this._set(this.state, 'writable', 2);
this._receiveMultiple(obj);
Expand Down
7 changes: 4 additions & 3 deletions client/vite.config.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
import { defineConfig } from "vite";
import dtsPlugin from "vite-plugin-dts";
import autoExternalPlugin from "rollup-plugin-auto-external";

export default defineConfig({
plugins: [
dtsPlugin({ rollupTypes: true }),
autoExternalPlugin()
dtsPlugin({ rollupTypes: true })
],
build: {
sourcemap: true,
Expand All @@ -14,6 +12,9 @@ export default defineConfig({
entry: './src/client.ts',
fileName: () => 'facilmap-client.mjs',
formats: ['es']
},
rollupOptions: {
external: (id) => !id.startsWith("./") && !id.startsWith("../") && /* resolved internal modules */ !id.startsWith("/")
}
}
});
56 changes: 55 additions & 1 deletion docs/src/developers/client/advanced.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,4 +64,58 @@ class ReactiveClient extends Client {
});
}
}
```
```

### React

```javascript
class ObservableClient extends Client {
_observers = new Set();

subscribe(callback) {
this._observers.add(callback);
return () => {
this._observers.delete(callback);
};
}

_triggerObservers() {
for (const observer of this._observers) {
observer();
}
}

_set(object, key, value) {
object[key] = value;
this._triggerObservers();
}

_delete(object, key) {
delete object[key];
this._triggerObservers();
}
}

function useClientObserver(client, selector) {
React.useSyncExternalStore(
(callback) => client.subscribe(callback),
() => selector(client)
);
}

const MarkerInfo = ({ client, markerId }) => {
const marker = useClientObserver(client, (client) => client.markers[markerId]);
return (
<div>
Marker name: {marker?.name}
</div>
);
}
```
Keep in mind that React’s `useSyncExternalStore` will rerender the component if the resulting _object reference_ changes.
This means one the one hand that you cannot use this example implementation on a higher up object of the client (such as
`client` itself or `client.markers`), as their identity never changes, causing your component to never rerender. And on
the other hand that you should avoid using it on objects created in the selector (such as returning
`[client.padData.id, client.padData.name]` in order to get multiple values at once), as it will cause your component to
rerender every time the selector is called.
53 changes: 39 additions & 14 deletions docs/src/developers/frontend/README.md
Original file line number Diff line number Diff line change
@@ -1,33 +1,58 @@
# Overview

The FacilMap frontend is a [Vue.js](https://vuejs.org/) app that provides the main FacilMap UI.
The FacilMap frontend is a [Vue.js](https://vuejs.org/) app that provides the main FacilMap UI. You can use it to integrate a modified or extended version of the FacilMap UI or its individual components into your app. If you just want to embed the whole FacilMap UI without any modifications, it is easier to [embed it as an iframe](../embed.md).

The FacilMap frontend is available as the [facilmap-frontend](https://www.npmjs.com/package/facilmap-frontend) package on NPM.

Right now there is no documentation of the individual UI components, nor are they designed to be reusable or have a stable interface. Use them at your own risk and have a look at the [source code](https://github.com/FacilMap/facilmap/tree/main/frontend/src/lib) to get an idea how to use them.

## Setup

The FacilMap frontend uses [Bootstrap-Vue](https://bootstrap-vue.org/), but does not install it by default to provide bigger flexibility. When using the FacilMap frontend, make sure to have it [set up](https://bootstrap-vue.org/docs#using-module-bundlers):
The FacilMap frontend is published as an ES module. It is meant to be used as part of an app that is built using a bundler, rather than importing it directly into a HTML file.

The frontend heavily relies on Vue, Bootstrap and other large libraries. These are not part of the bundle, but are imported using `import` JavaScript statements. This avoids duplicating these dependencies if you also use them elsewhere in your app.

To get FacilMap into your app, install the NPM package using `npm install -S facilmap-frontend` or `yarn add facilmap-frontend` and then import the components that you need.

```javascript
import Vue from "vue";
import { BootstrapVue } from "bootstrap-vue";
import "bootstrap/dist/css/bootstrap.css";
import "bootstrap-vue/dist/bootstrap-vue.css";
import { FacilMap } from "facilmap-frontend";
```

The FacilMap UI uses a slightly adjusted version of [Bootstrap 5](https://getbootstrap.com/) for styling. To avoid duplication if you want to integrate FacilMap into an app that is already using Bootstrap, the Bootstrap CSS is not part of the main export. If you want to use FacilMap’s default Bootstrap styles, import them separately from `facilmap-frontend/bootstrap.css`:

Vue.use(BootstrapVue);
```javascript
import "facilmap-frontend/bootstrap.css";
```

## Structure

The FacilMap server renders a static HTML file, which already contains some metadata about the map (such as the map title and the search engines policy configured for the particular map). It then renders a Vue.js app that renders the FacilMap UI using the [`FacilMap`](./facilmap.md) component. It sets the props and listens to the events of the FacilMap app in a way that the URL and document title are updated as the user opens or closes collaborative maps or their metadata changes.
The [`<FacilMap>`](./facilmap.md) component renders the whole frontend, including a component that provides a connection to the FacilMap server. If you want to render the whole FacilMap UI, simply render that component rather than rendering all the components of the UI individually.

The FacilMap frontend makes heavy use of [provide/inject](https://vuejs.org/v2/api/#provide-inject) feature of Vue.js. Most FacilMap components require the presence of certain injected objects to work. When rendering the `FacilMap` component, the following component hierarchy is created: `FacilMap` (provides `Context`) → `ClientProvider` (provides `Client`) → `LeafletMap` (provides `MapComponents` and `MapContext`) → any UI components. The injected objects have the following purpose:
* `Context`: A reactive object that contains general details about the context in which this FacilMap runs, such as the props that were passed to the `FacilMap` component and the currently opened map ID.
* `Client`: A reactive instance of the FacilMap client.
* `MapComponents`: A non-reactive object that contains all the Leaflet components of the map.
* `MapContext`: A reactive object that contains information about the current state of some Leaflet components, for example the current position of the map. It also acts as an event emitter that is used for communication between different components of the UI.
```vue
<FacilMap
baseUrl="https://facilmap.org/"
serverUrl="https://facilmap.org/"
:padId="undefined"
></FacilMap>
```

The `<FacilMapContextProvider>` component provides a reactive object that acts as the central hub for all components to provide their public state (for example the facilmap-client object, the current map ID, the current selection, the current map view) and their public API (for example to open a search box tab, to open a map, to set route destinations). All components that need to communicate with each other use this context for that. This means that if you want to render individual UI components, you need to make sure that they are rendered within a `<FacilMapContextProvider>` (or within a `<FacilMap>`, which renders the context provider for you). It also means that if you want to add your own custom UI components, they will benefit greatly from accessing the context.

The context is both injected and exposed by both the `<FacilMap>` and the `<FacilMapContextProvider>` component. To access the injected context, import the `injectContextRequired` function from `facilmap-frontend` and call it in the setup function of your component. For this, the component must be a child of `<FacilMap>` or `<FacilMapContextProvider>`. To access the exposed context, use a ref:
```vue
<script setup lang="ts">
import { FacilMap } from "facilmap-frontend";
const facilMapRef = ref<InstanceType<typeof FacilMap>>();
// Access the context as facilMapRef.value.context
</script>
<template>
<FacilMap ref="facilMapRef"></FacilMap>
</template>
```

By passing child components to the `FacilMap` component, you can yourself make use of these injected objects when building extensions for FacilMap. When using class components with [vue-property-decorator](https://github.com/kaorun343/vue-property-decorator), you can inject these objects by using the `InjectContext()`, `InjectMapComponents()`, … decorators. Otherwise, you can inject them by using `inject: [CONTEXT_INJECT_KEY, MAP_COMPONENTS_INJECT_KEY, …]`.
For now there is no documentation of the context object. To get an idea of its API, have a look at its [source code](https://github.com/FacilMap/facilmap/blob/main/frontend/src/lib/components/facil-map-context-provider/facil-map-context.ts).

## Styling

Expand Down
62 changes: 30 additions & 32 deletions docs/src/developers/frontend/facilmap.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,53 +3,51 @@
The `FacilMap` component renders a complete FacilMap UI. It can be used like this in a Vue.js app:

```vue
<template>
<FacilMap base-url="/" server-url="https://facilmap.org/" pad-id="my-map"></FacilMap>
</template>
<script>
<script setup>
import { FacilMap } from "facilmap-frontend";
export default {
components: { FacilMap }
};
</script>
<template>
<FacilMap
baseUrl="https://facilmap.org/"
serverUrl="https://facilmap.org/"
padId="my-map"
></FacilMap>
</template>
```

In a non-Vue.js app, it can be embedded like this:

```javascript
import { FacilMap } from "facilmap-frontend";
import Vue from "vue";

new Vue({
el: "#facilmap", // A selector whose DOM element will be replaced with the FacilMap component
components: { FacilMap },
render: (h) => (
h("FacilMap", {
props: {
baseUrl: "/",
serverUrl: "https://facilmap.org/",
padId: "my-map"
}
})
)
});
import Vue, { createApp, defineComponent, h } from "vue";

createApp(defineComponent({
setup() {
return () => h(FacilMap, {
baseUrl: "https://facilmap.org/",
serverUrl: "https://facilmap.org/",
padId: "my-map"
});
}
})).mount(document.getElementById("facilmap")!); // A DOM element that be replaced with the FacilMap component
```

## Props

Note that all of these props are reactive and can be changed while the map is open.

* `baseUrl` (string, required): Collaborative maps should be reachable under `${baseUrl}${mapId}`, while the general map should be available under `${baseUrl}`. For the default FacilMap installation, `baseUrl` would be `https://facilmap.org/` (or simply `/`). It needs to end with a slash. It is used to create the map URL for example in the map settings or when switching between different maps (only in interactive mode).
* `baseUrl` (string, required): Collaborative maps should be reachable under `${baseUrl}${mapId}`, while the general map should be available under `${baseUrl}`. For the default FacilMap installation, `baseUrl` would be `https://facilmap.org/`. It needs to end with a slash. It is used to create the map URL for example in the map settings or when switching between different maps (only in interactive mode).
* `serverUrl` (string, required): The URL under which the FacilMap server is running, for example `https://facilmap.org/`. This is invisible to the user.
* `padId` (string, optional): The ID of the collaborative map that should be opened. If this is undefined, no map is opened. This is reactive, when a new value is passed, a new map is opened. Note that the map ID may change as the map is open, either because the ID of the map is changed in the map settings, or because the user navigates to a different map (only in interactive mode). Use `:padId.sync` to get a [two-way binding](https://vuejs.org/v2/guide/components-custom-events.html#sync-Modifier) (or listen to the `update:padId` event).
* `toolbox` (boolean, optional): Whether the toolbox should be shown. Default is `true`.
* `search` (boolean, optional): Whether the search box should be shown. Default is `true`.
* `autofocus` (boolean, optional): Whether the search field should be focused. Default is `false`.
* `legend` (boolean, optional): Whether the legend should be shown (if it is available). Default is `true`.
* `interactive` (boolean, optional): Whether [interactive mode](../embed.md#interactive-mode) should be enabled. Default is `true`.
* `linkLogo` (boolean, optional): If `true`, the FacilMap logo will be a link that opens the map in a new window. Default is `false`.
* `updateHash` (boolean, optional): Whether `location.hash` should be synchonised with the current map view. Default is `false`.
* `padId` (string or undefined, required): The ID of the collaborative map that should be opened. If this is undefined, no map is opened. This is reactive, when a new value is passed, a new map is opened. Note that the map ID may change as the map is open, either because the ID of the map is changed in the map settings, or because the user navigates to a different map (only in interactive mode). Use `v-model:padId` to get a [two-way binding](https://vuejs.org/guide/essentials/forms.html) (or listen to the `update:padId` event).
* `settings` (object, optional): An object with the following properties:
* `toolbox` (boolean, optional): Whether the toolbox should be shown. Default is `true`.
* `search` (boolean, optional): Whether the search box should be shown. Default is `true`.
* `autofocus` (boolean, optional): Whether the search field should be focused. Default is `false`.
* `legend` (boolean, optional): Whether the legend should be shown (if it is available). Default is `true`.
* `interactive` (boolean, optional): Whether [interactive mode](../embed.md#interactive-mode) should be enabled. Default is `true`.
* `linkLogo` (boolean, optional): If `true`, the FacilMap logo will be a link that opens the map in a new window. Default is `false`.
* `updateHash` (boolean, optional): Whether `location.hash` should be synchonised with the current map view. Default is `false`.

## Events

Expand Down
3 changes: 3 additions & 0 deletions frontend/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
**facilmap-frontend** is the Vue.js frontend for [FacilMap](https://github.com/facilmap/facilmap).

Some information on how to use it can be found in the [developer documentation](https://docs.facilmap.org/developers/frontend/).
4 changes: 2 additions & 2 deletions frontend/build.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@ import { dirname } from "path";
import { fileURLToPath } from "url";

const root = dirname(fileURLToPath(import.meta.url));
const dist = `${root}/dist`;
const dist = `${root}/dist/app`;

export const paths = {
root,
dist,
base: '/app/',
base: '/_app/',
mapEntry: "src/map/map.ts",
mapEjs: `${root}/src/map/map.ejs`,
tableEntry: "src/table/table.ts",
Expand Down
10 changes: 6 additions & 4 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,18 @@
"url": "https://github.com/FacilMap/facilmap.git"
},
"type": "module",
"main": "./dist/frontend.js",
"typings": "./dist/src/lib/index.d.ts",
"main": "./dist/lib/facilmap-frontend.mjs",
"typings": "./dist/lib/facilmap-frontend.d.ts",
"files": [
"dist",
"src",
"static",
"iframe-test.html",
"README.md",
"tsconfig.json"
"tsconfig.json",
"build.js",
"build.d.ts",
"public"
],
"scripts": {
"build": "yarn build:lib && yarn build:app",
Expand Down Expand Up @@ -65,7 +68,6 @@
"p-debounce": "^4.0.0",
"pluralize": "^8.0.0",
"popper-max-size-modifier": "^0.2.0",
"rollup-plugin-auto-external": "^2.0.0",
"tablesorter": "^2.31.3",
"vite": "^4.5.0",
"vite-plugin-css-injected-by-js": "^3.3.0",
Expand Down
58 changes: 28 additions & 30 deletions frontend/src/example/example-root.vue
Original file line number Diff line number Diff line change
Expand Up @@ -14,35 +14,33 @@
</script>

<template>
<div>
<FacilMap
:baseUrl="serverUrl"
:serverUrl="serverUrl"
v-model:padId="padId1"
@update:padName="padName1 = $event"
ref="map1Ref"
>
<template #before>
<div>{{padId1}} | {{padName1}}</div>
<div>
#{{map1Ref?.context?.components.map?.hash}}
</div>
</template>
</FacilMap>
<FacilMap
:baseUrl="serverUrl"
:serverUrl="serverUrl"
v-model:padId="padId1"
@update:padName="padName1 = $event"
ref="map1Ref"
>
<template #before>
<div>{{padId1}} | {{padName1}}</div>
<div>
#{{map1Ref?.context?.components.map?.hash}}
</div>
</template>
</FacilMap>

<FacilMap
:baseUrl="serverUrl"
:serverUrl="serverUrl"
v-model:padId="padId2"
@update:padName="padName2 = $event"
ref="map2Ref"
>
<template #before>
<div>{{padId2}} | {{padName2}}</div>
<div>
#{{map2Ref?.context?.components.map?.hash}}
</div>
</template>
</FacilMap>
</div>
<FacilMap
:baseUrl="serverUrl"
:serverUrl="serverUrl"
v-model:padId="padId2"
@update:padName="padName2 = $event"
ref="map2Ref"
>
<template #before>
<div>{{padId2}} | {{padName2}}</div>
<div>
#{{map2Ref?.context?.components.map?.hash}}
</div>
</template>
</FacilMap>
</template>
1 change: 1 addition & 0 deletions frontend/src/example/example.html
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
width: 50%;
}
</style>
<script type="module" src="./example.ts"></script>
</head>
<body>
<div id="app"></div>
Expand Down
Loading

0 comments on commit f4555ee

Please sign in to comment.