The Editor Component
A core part of shacl-vue
is its ability to render a dynamic editor component (and in future also a viewer component) based on the specific constraints in a SHACL PropertyShape
. E.g. if the sh:datatype
of the shape is xsd:datetime
, then one would expect the input field to be a date/time selector, or if the sh:NodeKind
is sh:IRI
, then one would expect to be able to select from a list of existing resource identifiers or add a new one.
Here we provide more information about how editor components are put together in order to achieve this dynamic redering, how matching works, how to create new custom editor components, and how to make them discoverable to the application.
The editor component internals
WARNING
shacl-vue
is under continuous development and might change at any time. Abstracting out general functionality, especially related to base editor code, is likely to affect the descriptions below.
Let's use the HexEditor.vue code as an example to explain the component internals.
The template
<template>
<v-input
v-model="internalValue"
:rules="rules"
ref="fieldRef"
:id="inputId"
hide-details="auto"
style="margin-bottom: 1em;"
>
<v-text-field
v-model="subValues.hex_text"
density="compact"
variant="outlined"
label="add hexadecimal text"
hide-details="auto"
>
</v-text-field>
</v-input>
</template>
The template part of the component defines the look, feel, and UI-related functionality of the component. As you can see, there is a Vuetify v-input
component that wraps the component, which is used to provide a unified API for any custom editor. See this commit for more details. All attributes of the v-input
tag are required to be reused as is in any new custom component, except for the style
attribute:
v-model="internalValue"
provides a two-way binding of thev-input
to a requiredinternalValue
, which is automatically reflected in the globalformData
state. In other words, this is the actual value of the editor component as input by the user.:rules="rules"
binds therules
variable (further explained in thesetup
script section below) to therules
property of thev-input
component, for validation purposesref="fieldRef"
and:id="inputId"
assigns aref
andid
, respectively, to thev-input
component, which allows any component to be referenced (possibly recursively) by its parent form, for validation purposes (see this commit).hide-details="auto"
makes for a better UX regarding messaging from thev-input
component.
The custom nature of an editor component is established by adding any number of components inside the v-input
. In the above example we have a single v-text-field
, but this could also be multiple input components, such as the URIEditor
that is shipped with shacl-vue
. The important part is to v-model
each subcomponent with a unique field in the subValues
object, e.g. subValues.hex_text
above. These subvalues are used in a deterministic way when determining the internalValue
of the wrapping v-input
component.
The setup
script
<script setup>
import { useRules } from '../composables/rules'
import { useRegisterRef } from '../composables/refregister';
import { useBaseInput } from '@/composables/base';
const props = defineProps({
modelValue: String,
property_shape: Object,
node_uid: String,
triple_uid: String,
triple_idx: Number
})
const { rules } = useRules(props.property_shape)
const inputId = `input-${Date.now()}`;
const { fieldRef } = useRegisterRef(inputId, props);
const emit = defineEmits(['update:modelValue']);
const { subValues, internalValue } = useBaseInput(
props,
emit,
valueParser,
valueCombiner
);
function valueParser(value) {
// Parsing internalValue into ref values for separate subcomponent(s)
return {
hex_text: value,
}
}
function valueCombiner(values) {
// Determing internalValue from subvalues/subcomponents
return values.hex_text
}
</script>
This script defines the behavior logic for the custom component and runs before the component mounts (see the VueJS docs).
Importantly, this is where general functionality from composables are imported and used to allow the custom editor component to behave as expected:
- the
props
structure defines all arguments passed to the component from its parent - the
useRules
composable returns therules
variable that is constructed in the format expected by thev-input
component, by parsing constraints of thesh:PropertyShape
being processed. const inputId = `input-${Date.now()}`;
establishes a unique id for thev-input
field that is required for the next step.
INFO
Date.now()
is not strictly necessary as a way to get a unique id, and any alternative method can also be used instead.
- the
useRegisterRef
returns the unique ref that thev-input
is tagged with in order to allow it to be uniquely identified within the context of its parent form (FormEditor
component) in order to validate the form and list validation errors. const emit = defineEmits(['update:modelValue']);
tells the component to emit any local updates to themodelValue
prop upwards to the parent- the
useBaseInput
composable is essential to the behavior of a custom component. It should be provided with thevalueParser
andvalueCombiner
functions that are unique to the custom component, and it returnsinternalValue
which thev-input
is modeled with, and also thesubValues
structure. valueParser(value)
is the custom logic that the component should provide to determine the subvalues of possible subcomponents frominternalValue
valueCombiner(values)
is the custom logic that the component should provide to determine theinternalValue
from the subvalues of possible subcomponents
The functions in the above example are simplistic, since they just mirror the single subvalue. But this logic could also be more complex, see for example the functions associated with the URIEditor
component.
The matching script
<script>
import { SHACL, XSD } from '../modules/namespaces'
export const matchingLogic = (shape) => {
// sh:nodeKind exists
if ( shape.hasOwnProperty(SHACL.nodeKind.value) ) {
// sh:nodeKind == sh:Literal
if ( shape[SHACL.nodeKind.value] == SHACL.Literal.value ) {
// sh:datatype exists
if ( shape.hasOwnProperty(SHACL.datatype.value) ) {
// sh:datatype == xsd:hexBinary
return shape[SHACL.datatype.value] == XSD.hexBinary.value
}
}
}
return false
};
</script>
This is an extra and required script that specifies the logic for deciding whether a custom editor component should be rendered or not based on the sh:PropertyShape
. Here, for example, the component will match (return true
) if:
- the shape contains the
sh:NodeKind
field and - the
sh:NodeKind
is ansh:Literal
and - the shape contains the
sh:datatype
field and - the
sh:datatype
equalsXSD.hexBinary
INFO
TODO: the current matching procedure will return a boolean value when matched, which means that it could be possible for multiple components to match if their logic is similar. At the moment the matching procedure will select the first match. This logic will likely be replaced in future with a rating scheme, where a integer/decimal value of priority will be assigned to a given match based on some global configuration. The highest rated match would then be rendered.
Component discovery
Custom components can be created as outlined above and will then need to be placed inside the shacl-vue/src/components
directory in order to be auto-discovered by the application.
Component matching
The editors.js
module, which is used once in the MainForm
component upon application startup, provides the necessary code for grabbing the matching logic of all custom Vue components and making that available (via Provide/Inject
) to the PropertyShapeEditor
editor that dynamically matches the correct editor component to the sh:PropertyShape
.
In the PropertyShapeEditor
, a computed property determines the correct match:
const matchedComponent = computed(() => {
for (const key in editorMatchers) {
if (editorMatchers[key].match(props.property_shape)) {
return editorMatchers[key].component;
}
}
return defaultEditor;
});
If no match is found, a DefaultEditor
is returned, which is currently set to the UnknownEditor
which just prints a line and contains no input field.