apprt-vue
This bundle provides support for the Vue.js framework to develop bundles that have a user interface following the MVVM pattern.
Usage
First, create a Vue component using apprt-vue/Vue
based on a Vue file:
import Vue from "apprt-vue/Vue";
import MyVueComponent from "./MyVueComponent.vue";
let myVueComponent = new Vue(MyVueComponent);
To integrate the Vue component into a map.apps template, it needs to be a Dijit Widget.
For this purpose you can use the apprt-vue/VueDijit
as a wrapper around your Vue component:
import VueDijit from "apprt-vue/VueDijit";
let dijitWidget = VueDijit(myVueComponent);
See also the bundle's API documentation for more details.
Use Cases
Adding a custom CSS class to a VueDijit
To add a class to the root node of a VueDijit (in parallel with the class vue-base
), use:
import Vue from "apprt-vue/Vue";
import MyVueComponent from "./MyVueComponent.vue";
let myVueComponent = new VueDijit(MyVueComponent, { class: "myAdditionalStyleClass" });
Interacting with reactive data (based on the reactivity API)
NOTE:
useReactiveSnapshot()
and the reactivity API are still actively being worked on. There may be breaking changes based on user feedback; stability can not yet be guaranteed.
Use useReactiveSnapshot
to read reactive data (that uses the reactivity API) in a Vue component.
Simple example:
<script setup lang="ts">
import { useReactiveSnapshot } from "apprt-vue";
import { type CountModel } from "./somewhere";
// (1)
const props = defineProps<{
model: CountModel;
}>();
// (2)
const snapshot = useReactiveSnapshot(() => {
return {
count: props.model.count
};
});
</script>
<template>
<!-- (3) -->
<div>Current count: {{ snapshot.count }}</div>
</template>
The component renders the current .count
of the model
:
-
(1) The component gets some reactive object, as a prop in this case.
model
is implemented using the reactivity API, and not using vue's reactivity.WARNING: Be careful when passing "rich" objects around as Vue data or props. Vue must be prevented from observing deep object graphs (for example map.apps components or ArcGIS objects). See Passing models to vue components below.
-
(2) The
useReactiveSnapshot
composable evaluates thecompute()
function and returns its result (the "snapshot"). It also subscribes to changes ofmodel.count
(or any other reactive values that were accessed) and automatically updates the snapshot when necessary. -
(3) The Vue template renders the snapshot. Since the snapshot is always up-to-date, the component will always render the current value.
Things to keep in mind:
- The snapshot updates on its own.
- You can have multiple snapshots in the same component.
- You should not write to the snapshot; treat it as read-only data instead. If you need to change the model in some way, write to it directly (or call a method).
- You can make the
compute
function ofuseReactiveSnapshot
as complex as you like. However, it should be rather fast and it should not have side effects: if there are many changes, the function will run often as well. - Due to problems with Vue 2.x's reactivity system, you should not pass complex objects around
as props or Vue-data (including Vue's
ref()
andreactive()
). However, you can useshallowRef()
both outside and inside your Vue-components to pass those objects (see also Passing models to Vue components)
Updating data
useReactiveSnapshot
intentionally provides one-way data binding only: whenever the reactive data changes, the vue component receives a current snapshot.
While you should refer to the current snapshot
for display purposes, updating data should happen via writable properties or methods; just like from "normal" (non-Vue) code.
The following example renders and increments a counter when the button is clicked:
<script setup lang="ts">
import { useReactiveSnapshot } from "apprt-vue";
import { type CountModel } from "./somewhere";
const props = defineProps<{
model: CountModel;
}>();
const snapshot = useReactiveSnapshot(() => {
return {
count: props.model.count
};
});
</script>
<template>
<div>
Current count: {{ snapshot.count }}
<!-- Note: calls a method on the model instead of changing `snapshot.count`!-->
<button @click="props.model.incrementCount()">Increment</button>
</div>
</template>
Passing models to Vue components
By default, Vue 2.x will deeply observe an entire object graph (the object and all objects reachable via references from that object) whenever a new object is being used in a (Vue-) reactive context.
This applies in props
, data
, and, when using the composition API, reactive()
and ref()
etc.
To avoid correctness or performance issues, Vue 2.x must be prevented from seeing complex object structures, especially map.apps components or ArcGIS object: their implementation uses references heavily (sometimes in a circular fashion).
For example, observing a single Graphic
could end up observing the entire Map
with all its child objects.
There are multiple workarounds:
- Use
provide
/inject
to share references to complex objects.provide
is not reactive, so the problem cannot occur. - Use non-reactive properties (e.g.
vm._someProperty = someObject
where someObject was not declared in the Vue component's definition). - With Vue 2.7, one can use the composition API's
shallowRef()
. By wrapping an object withshallowRef()
, we can tell Vue not to do deep observation. This is the recommended solution for modern versions of map.apps, but it does require some discipline and care.
Example (shallowRef)
The following snippet instantiates the Vue component shown above in Updating data.
import { shallowRef } from "vue";
import CountUI from "./CountUI.vue";
const CountUIComponent = Vue.extend(CountUI); // Creates a "real" Vue class, not always needed
const model = new CountModel(/* ... */); // definition not shown
const vm = new CountUIComponent({
propsData: {
// BAD: This would be wrong
//model: model
// GOOD: this works as expected
model: shallowRef(model)
}
});
You can verify this manually by inspecting the relevant objects at runtime in your debugger.
When Vue 2.x observes an object, it adds the __ob__
property.
If it's missing, you're good to go.
Create a binding between a Vue component and business models based on Bindable
To make synchronization easier between a Vue component and a component such as esri/core/Accessor
, use the apprt-binding/Binding
.
To ensure that your Vue component supports the Bindable
interface defined by apprt-binding
, use the mixin apprt-vue/mixins/Bindable
.
// in your .vue file <script> tag:
import Bindable from "apprt-vue/mixins/Bindable";
export default {
...
mixins: [Bindable]
}
Now your component supports the required methods.
import Binding from "apprt-binding/Binding"
import MyVueComponent from "./MyVueComponent.vue";
let vm = new Vue(MyVueComponent);
let myaccessor = ...
Binding.for(myAccessor,vm)
.sync("message", "input")
.enable();
Integrate external content in your .vue
file
To integrate external content into your VueWidget
, provide a DOM node in your template with the component apprt-vue/CtDomNode
as in the following sample:
<template>
<ct-dom-node :node="domNode"></ct-dom-node>
</template>
<script>
import CtDomNode from "apprt-vue/CtDomNode";
export default {
props: ["domNode"],
components: {
"ct-dom-node": CtDomNode
}
};
</script>
Now you can set the external DOM node to your component as in the following sample:
import Vue from "apprt-vue/Vue";
import MyComponentDefinition from "./MyComponent.vue";
const myComponent = new Vue(MyComponentDefinition);
myComponent.domNode = d_construct.create("span", { innerHTML: "Hello World!" });