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"
>
<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-inputto a requiredinternalValue, which is automatically reflected in the globalformDatastate. In other words, this is the actual value of the editor component as input by the user.:rules="rules"binds therulesvariable (further explained in thesetupscript section below) to therulesproperty of thev-inputcomponent, for validation purposesref="fieldRef"and:id="inputId"assigns arefandid, respectively, to thev-inputcomponent, 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-inputcomponent.
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,
node_idx: 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) {
// Determine 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
propsstructure defines all arguments passed to the component from its parent - the
useRulescomposable returns therulesvariable that is constructed in the format expected by thev-inputcomponent, by parsing constraints of thesh:PropertyShapebeing processed. const inputId = `input-${Date.now()}`;establishes a unique id for thev-inputfield 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
useRegisterRefreturns the unique ref that thev-inputis tagged with in order to allow it to be uniquely identified within the context of its parent form (FormEditorcomponent) in order to validate the form and list validation errors. const emit = defineEmits(['update:modelValue']);tells the component to emit any local updates to themodelValueprop upwards to the parent- the
useBaseInputcomposable is essential to the behavior of a custom component. It should be provided with thevalueParserandvalueCombinerfunctions that are unique to the custom component, and it returnsinternalValuewhich thev-inputis modeled with, and also thesubValuesstructure. valueParser(value)is the custom logic that the component should provide to determine the subvalues of possible subcomponents frominternalValuevalueCombiner(values)is the custom logic that the component should provide to determine theinternalValuefrom 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:NodeKindfield and - the
sh:NodeKindis ansh:Literaland - the shape contains the
sh:datatypefield and - the
sh:datatypeequalsXSD.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. The component filename needs to end in ...Editor.vue, and the file 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 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.