Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor for modularity and reusability #65

Open
jsheunis opened this issue Dec 11, 2024 · 5 comments
Open

Refactor for modularity and reusability #65

jsheunis opened this issue Dec 11, 2024 · 5 comments

Comments

@jsheunis
Copy link
Collaborator

The shacl-vue app has a few core functionalities currently:

  • reading, understanding, and storing RDF generally, using rdf/js-based libraries
  • reading and understanding SHACL schemas, and translating them into an internal javascript object-based representation
  • Automatic form and form field generation and validation from a SHACL schema. This uses Vue+Vuetify components (which include code for UI and logic) that have embedded matching criteria linked to specific schema fields. This means that forms can dynamically render matched fields from a dynamic pool of matchable components.
  • Automatic viewer generation from a SHACL schema. This uses the same matching approach as the forms, while the UI components are different for viewing vs editing (forms).
  • Editing RDF metadata using both the viewer and editor componets.
  • Saving metadata from the internal object representation into RDF in a temporary in-browser rdf store

The current application is fairly modular already, with core functionality divided into vue components, vue composables, general modules, and general plugins that all support the above functionality. Even so, it could very well do with more refactoring to improve modularity and reusability of components, both Vue-based or conceptual components. In addition, the code in general does not (yet) make use of object-oriented programming approaches; this needs some exploration to see where and how it would be beneficial.

Three envisioned use cases provide additional context for this refactoring process:

  1. Using shacl-vue as a dependency. Developers might want to deploy their own custom site for whichever purpose, and only use a specific component from shacl-vue, e.g. metadata viewers, on said site. To support such uses, the various functionalities of shacl-vue should be appropriately sectioned and made interoperable, such that they can be imported into other applications as self-contained and fully functional modules/classes/functions.
  2. shacl-vue depending on other core libraries. This is already the case for RDF/JS-based libraries (e.g. rdf-ext, n3), but is conceivable that this list might grow in future, in order for shacl-vue to depend on open and widely used libraries that might already achieve some function that shacl-vue needs. E.g. shacl-vue currently supports only basic SHACL constraints, so if another library exists that supports a wider range of SHACL constraints and is also continuously maintained, shacl-vue could rather depend on that. See also for context: Questions about shacl-form ULB-Darmstadt/shacl-form#28
  3. A fully fledged metadata management app for editing, saving, viewing, importing, exporting, annotating metadata. In essence, this would just be additional UI and logic layers on top of the first two use cases. The current state of shacl-vue is pretty much operating in the domain of this use case, although far from fully fledged and far from appropriately refactored.
@jsheunis
Copy link
Collaborator Author

jsheunis commented Dec 12, 2024

Thinking of ways to achieve:

  1. Using shacl-vue as a dependency. Developers might want to deploy their own custom site for whichever purpose, and only use a specific component from shacl-vue, e.g. metadata viewers, on said site. To support such uses, the various functionalities of shacl-vue should be appropriately sectioned and made interoperable, such that they can be imported into other applications as self-contained and fully functional modules/classes/functions.

I am currently reading up on the following which I think will provide insight into how to use vue components in agnostic web environments:

From the first link:

You can use Vue to build standard Web Components that can be embedded in any HTML page, regardless of how they are rendered. This option allows you to leverage Vue in a completely consumer-agnostic fashion: the resulting web components can be embedded in legacy applications, static HTML, or even applications built with other frameworks.

Things to look into:

@jsheunis
Copy link
Collaborator Author

When redesigning the interface for instantiating (a part of) shacl-vue inside an arbitrary and different context, we should think about how such an application would want to instantiate specific features, and which inputs would be required for such features. Currently, the shacl-vue application loads the schema and existing data and additional required inputs (specifically: OWL containing subclass statements), all specified via either a config file or defaults, on on app startup (after reading the config file). These are required to support all shacl-vue functionality including form rendering for any class in the input schema as well as data display.

Now, the question is: how would other apps instantiate (a part of) shacl-vue? I can think of the following use cases:

  • a form/editor for a specific class
  • a form/editor for any user-selectable class (i.e. as it is currently, with the user being able to select a class from a dropdown list)
  • a viewer for all data in the current graph
  • a viewer for all data of a specific class in the current graph
  • all of the above, i.e. editing and viewing

In all cases a web component could be constructed to define the main purpose of the instantiation, e.g. via html tags such as ShaclVueEditor or ShaclVueViewer, and the inputs could then be specified as html attributes. Required inputs for all components would need to be:

  • the SHACL schema,
  • he OWL class data,
  • and any RDF data to inject into the namespace

In addition, any component that is class-specific would need to specify the relevant class name via another attribute.

This refactoring would affect where and how the required data are read, provided, injected, computed, etc. As mentioned, the input files are currently read on app startup. There is some discrepancy in how provide/inject is used in order to supply the current three main views (MainForm, MainData, MainViewer ) with their respective required data, and how composables are used to generate the globally accessible FormData and GraphData. These discrepancies will have to be reassessed in the light of this refactoring process, suggesting that FormData and GraphData might be instantiated and the provided/injected differently, and the composables might even be altered substantially.

More to follow...

@jsheunis
Copy link
Collaborator Author

I am trying to figure out how to structure the app's hierarchy in terms of classes, components, and composables. I intend to go the route of making user-facing features (such as a "form" or "data browser" or both) available as both regular vue components for vue apps and as custom elements for non-vue apps. The question now is which features to establish as these entry points, and how to structure the classes, components and composables accordingly. I already know that there will be a number of required inputs and derived variables for any feature instantiation. More specifically, if a user wants to instantiate either a SHACLVueForm or SHACLVueBrowser component (for entering and viewing data, respectively), or both, these instantiations would all require:

  • a SHACL shapes graph
  • an OWL file with class relationships
  • an RDF data graph
  • possibly some config data

Firstly, I am not sure what is common or best practice when it comes to reading/fetching data provided as input arguments to a component instantiation. Is it better to expect the surrounding user application to do this and to pass javascript objects to the component or custom element? Or is it better to be able to pass URLs and expect the classes/composables behind the instantiation to handle data loading? Or should both options be supported? I'm leaning towards letting the shacl-vue components handle all complexities, including data loading, which is what it does currently too. But I don't have any arguments for explicitly disallowing loaded/derived data as arguments. One thing to consider is loading time and its effect on user experience. I know that currently shacl-vue's loading of RDF data into the necessary structures can take several seconds, and increases noticeably for large graphs.

Secondly, I am also thinking whether it would be a good idea to just have a single entry point e.g. SHACLVue and then allow passing the rendering specification, i.e. should a form be displayed or a data browser or both or some other thing, as an argument. I like this idea as it reduces complexity of options, but I'm not sure yet whether this introduces other challenges down the line.

Whether I have a single or multiple entry points for rendered views, I do think there should be a single instantiation of a class that handles required data loading and structuring, and possibly includes further graph-related functions such as storing/editing form data, storing/editing graph data, etc. I am not sure yet how such a class instantiation would be used across the Vue component hierarchy, where currently the process depends mainly on the provide/inject mechanism and functionality from composables.

Thirdly, there is another feature that needs to be supported by the refactored application. To minimize the effects of upfront bulk data import/fetching, we could let it operate on the principle of only fetching specific subsets when necessary. For example, a user is completing a form for a Publication and they need to select Authors from a dropdown. The dropdown could be populated on-demand by a query to some known endpoint that would return triples with rdf:type as Author (with some caching to streamline the process). This implies that existing individual components such as InstancesSelectEditor.vue would have to be updated to know where to get the RDF terms from, and it necessitates the use of some endpoint specification. Could this be something like a config object that has a mapping between class names and their respective endpoints? If so, I am not sure yet where and how that would be best to specify. Should it rather be embedded into SHACL rules or SPARQL query specifications, meaning that it wouldn't be an externally provided config and a custom data fethcing process, but rather be contained in the SHACL shapes graph itself and it would expect shacl-vue to support such functionality as described by the SHACL standard? Perhaps the former option first and the more involved yet more correct option later?

@jsheunis
Copy link
Collaborator Author

jsheunis commented Jan 21, 2025

I have made some progress on this front, and I hope to create a PR with a somewhat major overhaul of the source code structure and deployment setup within the next day. What follows is a summary of the updated app.

Main goal

We want the main functionality of shacl-vue (forms, editing/annotating, browsing, viewing) to be usable by developers both as a Vue component (or components) inside an arbitrary Vue application, and as a web component (a.k.a. Vue custom elements) inside an arbitrary non-vue application. For this reason the Vite-based build process can be updated to build the necessary library modules for both cases, which can then be published to npm and to a CDN.

An example of using it in a Vue application:

import { createApp } from 'vue';
import { ShaclVue} from 'shacl-vue'; // after `npm install shacl-vue`
import App from './App.vue';

const app = createApp(App);
app.component('ShaclVue', ShaclVue);
app.mount('#app');
<template>
    <shacl-vue></shacl-vue>
</template>

An example of using it in a NON-Vue application via the npm package:

import { registerCustomElements } from 'shacl-vue'; // after `npm install shacl-vue`
// Register custom elements with the function exported by `elements.js`, see in new structure below
registerCustomElements();
// Use the custom element in the DOM
document.body.innerHTML = '<shacl-vue></shacl-vue>';

or via CDN (and with DOM injection via javascript, just for the fun of it):

<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/shacl-vue.umd.js"></script>
<script>
  const app = document.createElement('shacl-vue');
  document.body.appendChild(app);
</script>

In all of the examples above, the surrounding application can prepare whichever arguments are necessary and pass them along to the ShaclVue component, as props in the Vue application, or as tag attributes in the non-Vue application.

New structure

The new source code structure will look as follows (summarised):

.
├── LICENSE
├── README.md
├── dist
├── docs
├── index.html
├── jsconfig.json
├── package-lock.json
├── package.json
├── public
├── src
│   ├── App.vue
│   ├── assets
│   ├── classes
│   │   ├── ClassDataset.js
│   │   ├── FormVue.js
│   │   ├── RdfDataset.js
│   │   ├── RdfDatasetReactive.js
│   │   ├── ShaclVue.js
│   │   ├── ShapesDataset.js
│   │   └── index.js
│   ├── components
│   │   ...
│   │   ├── ShaclVue.vue
│   │   ...
│   ├── composables
│   ├── elements.js
│   ├── index.js
│   ├── main.js
│   ├── modules
│   └── plugins
└── vite.config.mjs

Main changes include:

  • The new src/components/ShaclVue.vue component that serves as a single entrypoint into all functionality. This is the component that can be included in another Vue app, and that will be built into a web component.
  • The src/elements.js file that contains the code for creating and mounting the custom element (i.e. web component) from the ShaclVue.vue component.
  • All new classes in src/classes. This is a major part of the refactoring process, and combines object oriented programming with VueJS's component+composable-based approach. Much of the code in the new classes will make several composables redundant.
  • vite.config.mjs is updated to use library mode in order to build distributions that can be imported from npm and included via CDN.
  • The main application at src/App.vue is changed to have minimal content and to rather mount the new ShaclVue component. This component then instantiates the main ShaclVue class, of which the constructor will instantiate all required classes in order to load shapes, data, class hierarchy, and all else that is needed for the whole application to function as intended.

TODO

  • Local testing needs to be done to ensure feature parity with the previous state of the application.
  • An npm and CDN build+deployment workflow should be set up
  • The application can also include a custom, non-vue html page in order to help test the generated web component
  • Design and set up a full test deployment of the updated app

@jsheunis
Copy link
Collaborator Author

jsheunis commented Jan 24, 2025

Progress on this issue is here: 82ef69e

The structure is substantially more understandable and modular. Basically all envisioned changes have been made apart from adding the build and deployment workflow. I was testing the changes and got stuck on some sort of memory leak or recursive reactivity update that I can't pin down yet, and this breaks the metadata editing workflow that is central to the UX. I have spent too much time on this and it is extremely frustrating, to say the least.

I will take a break from this and get back to it later.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant