Skip to content

Commit

Permalink
wip base editor and composable functionality with test case
Browse files Browse the repository at this point in the history
  • Loading branch information
jsheunis committed Aug 22, 2024
1 parent 72a14c9 commit fe9b9cb
Show file tree
Hide file tree
Showing 6 changed files with 355 additions and 23 deletions.
2 changes: 2 additions & 0 deletions src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
<v-tab :value="1"> <v-icon icon="mdi-list-box-outline"></v-icon>&nbsp;&nbsp; Editor</v-tab>
<v-tab :value="2"><v-icon icon="mdi-database"></v-icon>&nbsp;&nbsp;Data</v-tab>
<v-tab :value="3"><v-icon icon="mdi-book-open"></v-icon>&nbsp;&nbsp;Viewer</v-tab>
<v-tab :value="4"><v-icon icon="mdi-cog"></v-icon>&nbsp;&nbsp;Testing</v-tab>
</v-tabs>

<br>
Expand All @@ -20,6 +21,7 @@
<v-tabs-window-item :key="1" :value="1"> <MainForm/> </v-tabs-window-item>
<v-tabs-window-item :key="2" :value="2"> <MainData/> </v-tabs-window-item>
<v-tabs-window-item :key="3" :value="3"> <MainViewer/> </v-tabs-window-item>
<v-tabs-window-item :key="4" :value="4"> <TestComp/> </v-tabs-window-item>
</v-tabs-window>
</v-card>
</v-main>
Expand Down
118 changes: 118 additions & 0 deletions src/components/BaseEditor.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
<!--
This BaseEditor component is designed as a building block for creating custom
input components. It provides a consistent API and structure for both the parent
component that uses it and for developers who extend it. By extending this base
component, developers can focus on the unique aspects of their custom components
(such as a combination of specific input types or complex logic) while inheriting
standardized behavior for common tasks like data synchronization, validation, and
error management.

Follow these steps to create a custom input component that extends the BaseEditor
component. This assumes you are familiar with Vue.js and Vuetify, as well as the
Composition API:

- create a new .vue file in your components directory, for example: MyCustomEditor.vue
- in the template section, use the BaseEditor component as the primary wrapper
- pass the required props to BaseEditor that control its behavior: v-model,
label, rules, value-parser, and value-combiner. These props allow the parent
component to interact with your custom component.
- define all of your custom components within the names "inputs" slot:
`<template #inputs="{ subValues }"></template>`
- subcomponent values should be v-modeled using unique keys on the named slot's
`subValues` prop; for example v-model a v-text-field to `subValues.customTextField`
- implement the valueParser and valueCombiner functions that transform the parent
v-model value "internalValue" into subcomponent values and vice versa.
- TODO: include description of rules and validation
- TODO: include description of matching (although this is not influenced by the BaseEditor)

-->

<template>
<!-- v-input is used as the wrapper component for all editor/input components -->
<v-input
v-model="internalValue"
:error="computedError"
:error-messages="computedErrorMessages"
class="base-editor"
>
<!-- The default slot template provides scoped props for custom behaviour -->
<template #default="{ isFocused, isDirty }">
<v-row no-gutters>
<!-- Custom inputs are passed in via the named slot "inputs".
This is the meat for extending the BaseEditor component -->
<!-- Automatically binds the input handler to the input event -->
<slot name="inputs" :subValues="subValues" :bind-input="bindInput"></slot>
</v-row>
</template>
</v-input>
</template>

<script setup>
import { useBaseInput } from '@/composables/baseinput';
import { computed } from 'vue';

/**
* Props for the BaseEditor component.
* @typedef {Object} BaseEditorProps
* @property {string|number|Object} modelValue - The value bound to v-model.
* @property {Array} [rules] - An array of validation rules for the input.
* @property {Function} valueParser - A function to parse the modelValue into subcomponent values.
* @property {Function} valueCombiner - A function to combine subcomponent values into the modelValue.
*/

/**
* Emits for the BaseEditor component.
* @typedef {Object} BaseEditorEmits
* @property {Function} update:modelValue - Emits when the value changes.
*/

// Define props and emits
const props = defineProps({
modelValue: [String, Number, Object],
rules: Array,
valueParser: Function,
valueCombiner: Function
});
const emit = defineEmits(['update:modelValue']);

// Use the composable to manage the custom input logic
const { subValues, internalValue, updateValue } = useBaseInput(
props,
emit,
props.valueParser,
props.valueCombiner
);

/**
* Computes whether the input has validation errors based on the provided rules.
* @returns {boolean} True if there's any validation error, otherwise false.
*/
const computedError = computed(() => {
return props.rules?.some(rule => rule(internalValue.value) !== true);
});

/**
* Computes the list of error messages based on the validation rules.
* @returns {Array} An array of error messages.
*/
const computedErrorMessages = computed(() => {
return props.rules?.map(rule => rule(internalValue.value)).filter(msg => msg !== true) || [];
});

/**
* Automatically bind input events to the updateValue function for custom inputs.
* @param {Object} element - The Vue component instance of the input element.
*/
function bindInput(element) {
if (element && element.props && !element.props.onInput) {
element.props.onInput = updateValue;
}
}
</script>


<style scoped>
.base-editor {
/* Custom styling for the base component */
}
</style>
81 changes: 81 additions & 0 deletions src/components/CustomSimple.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
<template>
<BaseEditor
v-model="internalValue"
:value-parser="parseValue"
:value-combiner="combineValues"
>
<template #inputs="{ subValues }">
<v-col cols="6">
<v-text-field
v-model="subValues.part1"
label="Part 1"
></v-text-field>
</v-col>
<v-col cols="6">
<v-select
v-model="subValues.part2"
:items="selectItems"
label="Part 2"
></v-select>
</v-col>
</template>
</BaseEditor>
BaseEditor v-model value: {{ internalValue }}
</template>

<script setup>
import BaseEditor from '@/components/BaseEditor.vue';
// import { useBaseInput } from '@/composables/baseinput';
import { ref, watch, onBeforeMount} from 'vue'
const props = defineProps({
modelValue: String,
property_shape: Object,
node_uid: String,
triple_uid: String
});
const emit = defineEmits(['update:modelValue']);
// Internal reactive state
const internalValue = ref(props.modelValue);
// Watch for changes in modelValue prop
watch(() => props.modelValue, (newValue) => {
internalValue.value = newValue;
});
// Emit updates to parent
watch(internalValue, (newValue) => {
emit('update:modelValue', newValue);
});
// const { internalValue } = useBaseInput(
// props,
// emit,
// parseValue,
// combineValues
// );
const selectItems = ["kaas", "koek", "moer"]
onBeforeMount(() => {
console.log("...BeforeMount CustomSimple....")
console.log(props.modelValue)
})
// Parse the combined value into subcomponent values
function parseValue(value) {
if (value) {
const [part1, part2] = value.split('-');
return { part1: part1 || '', part2: part2 || '' };
} else {
return { part1: '', part2: '' };
}
}
// Combine subcomponent values into a single value
function combineValues(values) {
return `${values.part1}-${values.part2}`;
}
</script>

94 changes: 94 additions & 0 deletions src/components/TestComp.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
<template>

<v-sheet class="pa-4" border rounded elevation="2">

<!-- <component
:is="CustomSimple"
v-model="formData[node_uid][0][triple_uid][0]"
:property_shape="shape_obj"
:node_uid="node_uid"
:triple_uid="triple_uid"
>
</component> -->

<v-btn @click="logStuff">Log stuff</v-btn>

<v-switch v-model="kaas"></v-switch>

<span v-if="kaas">
<CustomSimple
v-model="formData[node_uid][0][triple_uid][0]"
:property_shape="shape_obj"
:node_uid="node_uid"
:triple_uid="triple_uid"
>
</CustomSimple>
</span>

<v-divider></v-divider>

{{ formData }}

<v-divider></v-divider>

{{ formData[node_uid] }}

<v-divider></v-divider>

{{ formData[node_uid].at(-1) }}

<v-divider></v-divider>

bla: {{ formData[node_uid].at(-1)[triple_uid] }}

<v-divider></v-divider>

blabla: {{ formData[node_uid].at(-1)[triple_uid].at(-1) }}

<v-divider></v-divider>


</v-sheet>

</template>

<script setup>
import CustomSimple from '@/components/CustomSimple.vue';
import { ref, inject, onBeforeMount} from 'vue'
import { useShapeData } from '@/composables/shapedata';
import { useFormData } from '@/composables/formdata';
const simpledata = ref(null)
const kaas = ref(false)
const {
formData,
add_empty_node,
add_empty_triple,
} = useFormData()
const shape_file_url = new URL("@/assets/shapesgraph.ttl", import.meta.url).href
const {
nodeShapes
} = useShapeData(shape_file_url)
const node_uid = 'https://concepts.datalad.org/s/prov/unreleased/Activity'
const triple_uid = 'https://concepts.datalad.org/s/thing/unreleased/description'
const shape_obj = nodeShapes.value[node_uid]
onBeforeMount(() => {
console.log("...BeforeMounet TestComp....")
add_empty_node(node_uid)
add_empty_triple(node_uid, triple_uid)
})
function logStuff() {
console.log(formData)
console.log(formData[node_uid])
console.log(formData[node_uid].at(-1)[triple_uid].at(-1))
}
</script>
23 changes: 0 additions & 23 deletions src/composables/base.js

This file was deleted.

60 changes: 60 additions & 0 deletions src/composables/baseinput.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
// composables/baseinput.js
import { ref, watch, computed } from 'vue';

/**
* Composable for managing the state and behavior of the BaseEditor component.
*
* @param {Object} props - The props passed to the custom input component.
* @param {Function} emit - The emit function to trigger events.
* @param {Function} valueParser - A function to parse the modelValue into subcomponent values.
* @param {Function} valueCombiner - A function to combine subcomponent values into the modelValue.
* @returns {Object} - Returns an object containing reactive references and methods for managing the input state.
*/

export function useBaseInput(props, emit, valueParser, valueCombiner) {
/**
* Reactive object to hold the individual values of subcomponents.
* @type {Object}
*/
const subValues = ref(valueParser(props.modelValue) || {});

/**
* Computed property to manage the internal value of the custom input component.
* - The getter combines the subcomponent values into the modelValue.
* - The setter parses the modelValue back into subcomponent values.
*/
const internalValue = computed({
get() {
return valueCombiner(subValues.value);
},
set(value) {
subValues.value = valueParser(value);
}
});

/**
* Watcher for the parent component's modelValue.
* Updates internal values when modelValue changes.
*/
watch(
() => props.modelValue,
(newValue) => {
if (newValue !== internalValue.value) {
internalValue.value = newValue;
}
}
);

/**
* Method to emit an update to the parent component when the subcomponent values change.
*/
function updateValue() {
emit('update:modelValue', internalValue.value);
}

return {
subValues,
internalValue,
updateValue
};
}

0 comments on commit fe9b9cb

Please sign in to comment.