Skip to content

Commit

Permalink
Improve API, initialise reactivity outside of class
Browse files Browse the repository at this point in the history
Use some more Vue-like API for properties and methods made available to the plugin, and run the reactivity initialization outside of the class so it isn't populated with a whole lot of reactivity methods that shouldn't be used by the class anyway.
  • Loading branch information
bennothommo committed Sep 6, 2023
1 parent b36dc3c commit d296919
Show file tree
Hide file tree
Showing 7 changed files with 231 additions and 182 deletions.
12 changes: 1 addition & 11 deletions packages/reactivity/src/abstracts/ReactivePluginBase.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,6 @@
import { PluginBase } from '@wintercms/snowboard';
import {
reactivityConstructor,
reactivityInitialize,
reactivityGetProperties,
reactivityCreateStore,
reactivityMapProperties,
reactivityTemplate,
reactivityMount,
} from './shared';

Expand All @@ -32,11 +27,6 @@ class ReactivePluginBase extends PluginBase {
}
}

ReactivePluginBase.prototype.reactivityInitialize = reactivityInitialize;
ReactivePluginBase.prototype.reactivityGetProperties = reactivityGetProperties;
ReactivePluginBase.prototype.reactivityCreateStore = reactivityCreateStore;
ReactivePluginBase.prototype.reactivityMapProperties = reactivityMapProperties;
ReactivePluginBase.prototype.reactivityTemplate = reactivityTemplate;
ReactivePluginBase.prototype.reactivityMount = reactivityMount;
ReactivePluginBase.prototype.$mount = reactivityMount;

export default ReactivePluginBase;
12 changes: 1 addition & 11 deletions packages/reactivity/src/abstracts/ReactiveSingleton.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,6 @@
import { Singleton } from '@wintercms/snowboard';
import {
reactivityConstructor,
reactivityInitialize,
reactivityGetProperties,
reactivityCreateStore,
reactivityMapProperties,
reactivityTemplate,
reactivityMount,
} from './shared';

Expand Down Expand Up @@ -33,11 +28,6 @@ class ReactiveSingleton extends Singleton {
}
}

ReactiveSingleton.prototype.reactivityInitialize = reactivityInitialize;
ReactiveSingleton.prototype.reactivityGetProperties = reactivityGetProperties;
ReactiveSingleton.prototype.reactivityCreateStore = reactivityCreateStore;
ReactiveSingleton.prototype.reactivityMapProperties = reactivityMapProperties;
ReactiveSingleton.prototype.reactivityTemplate = reactivityTemplate;
ReactiveSingleton.prototype.reactivityMount = reactivityMount;
ReactiveSingleton.prototype.$mount = reactivityMount;

export default ReactiveSingleton;
197 changes: 89 additions & 108 deletions packages/reactivity/src/abstracts/shared.js
Original file line number Diff line number Diff line change
@@ -1,46 +1,80 @@
import { createApp, reactive } from 'petite-vue';

/**
* Constructor for the Reactivity functionality.
* Mount reactivity to the DOM.
*
* This creates the necessary properties and modifies the constructor to initialize the Reactivity
* functionality.
* Finally, the reactivity will be mounted to the DOM on the given element (or template), and will
* then be created as an application in Vue. It will then be ready for use.
*/
function reactivityConstructor() {
if (this.reactivityInitialized) {
function reactivityMount(element, parent = null) {
if (this.$reactive) {
throw new Error('Reactivity already initialized for this instance');
}

this.reactivityInitialized = false;
this.reactivityStore = {};
this.reactivityElement = null;
let parentNode = parent;

// Wrap the instance's constructor to call the Reactivity initialisation after construct
if (typeof this.construct === 'function') {
this.baseConstruct = this.construct;
this.construct = (...args) => {
this.baseConstruct(...args);
this.reactivityInitialize();
};
if (document.body.contains(element) && !parent) {
parentNode = element.parentNode;
} else if (!document.body.contains(element) && !parent) {
parentNode = document.body;
}

if (!document.body.contains(element)) {
parentNode.appendChild(element);
}

createApp(this.$data).mount(element);
this.$el = element;

// Import next tick into plugin
this.$nextTick = this.$data.$nextTick;

// Prevent reactivity from being reset
this.$reactive = true;
Object.defineProperty(this, '$reactive', { configurable: false, writable: false });
}

/**
* Initialize the Reactivity functionality.
* Fetches the available template.
*
* This provides the lifecycle workflow in order to initialize reactivity and make it available in
* the plugin.
* If the object provides a template function or string, this will create a template and embed it
* into the DOM.
*
* In the case of a function, the function may also return a DOM element directly, which will be
* used to mount directly to a specified element.
*/
function reactivityInitialize() {
// Do not re-enable reactivity
if (this.reactivityInitialized) {
throw new Error('Reactivity already initialized for this instance');
function reactivityTemplate() {
let template = null;

if (typeof this.template === 'function') {
template = this.template();
} else {
template = this.template;
}

const mappable = this.reactivityGetProperties();
this.reactivityStore = reactive(this.reactivityCreateStore(mappable));
this.reactivityMapProperties(mappable);
this.reactivityTemplate();
if (!template) {
return;
}

if (typeof template === 'string') {
const parser = new DOMParser();
const rendered = parser.parseFromString(template, 'text/html');
if (rendered.body.childElementCount > 1) {
throw new Error('Template must only have one root node');
}
if (rendered.body.firstElementChild instanceof HTMLTemplateElement) {
throw new Error('A string template must not return a template element');
}
reactivityMount.call(this, rendered.body.firstElementChild);
} else if (template instanceof HTMLTemplateElement) {
if (template.content.childElementCount > 1) {
throw new Error('Template must only have one root node');
}
const cloned = template.content.firstElementChild.cloneNode(true);
reactivityMount.call(this, cloned);
} else if (template instanceof HTMLElement) {
reactivityMount.call(this, template);
}
}

/**
Expand All @@ -58,11 +92,6 @@ function reactivityInitialize() {
* @return {Object}
*/
function reactivityGetProperties() {
// Do not re-enable reactivity
if (this.reactivityInitialized) {
throw new Error('Reactivity already initialized for this instance');
}

const mappable = {};
const props = Object.getOwnPropertyDescriptors(this);
const protoProps = Object.getOwnPropertyDescriptors(Object.getPrototypeOf(this));
Expand All @@ -76,6 +105,10 @@ function reactivityGetProperties() {
'destruct',
'destructor',
'detach',
'$reactive',
'$mount',
'$el',
'$data',
];

Object.entries(protoProps).forEach(([key, prop]) => {
Expand Down Expand Up @@ -135,11 +168,6 @@ function reactivityGetProperties() {
* @return {Object}
*/
function reactivityCreateStore(mappable) {
// Do not re-enable reactivity
if (this.reactivityInitialized) {
throw new Error('Reactivity already initialized for this instance');
}

const obj = {};

Object.entries(mappable).forEach(([key, prop]) => {
Expand Down Expand Up @@ -177,104 +205,57 @@ function reactivityCreateStore(mappable) {
* be accessed directly from the plugin instance and will react accordingly.
*/
function reactivityMapProperties(mappable) {
// Do not re-enable reactivity
if (this.reactivityInitialized) {
throw new Error('Reactivity already initialized for this instance');
}

Object.entries(mappable).forEach(([key, prop]) => {
if (prop.type === 'function' || prop.type === 'getter') {
Object.defineProperty(
this,
key,
Object.getOwnPropertyDescriptor(this.reactivityStore, key),
Object.getOwnPropertyDescriptor(this.$data, key),
);
} else {
Object.defineProperty(this, key, {
get() { return this.reactivityStore[key]; },
set(value) { this.reactivityStore[key] = value; },
get() { return this.$data[key]; },
set(value) { this.$data[key] = value; },
});
}
});
}

/**
* Fetches the available template.
*
* If the object provides a template function or string, this will create a template and embed it
* into the DOM.
* Initialize the Reactivity functionality.
*
* In the case of a function, the function may also return a DOM element directly, which will be
* used to mount directly to a specified element.
* This provides the lifecycle workflow in order to initialize reactivity and make it available in
* the plugin.
*/
function reactivityTemplate() {
// Do not re-enable reactivity
if (this.reactivityInitialized) {
throw new Error('Reactivity already initialized for this instance');
}

if (typeof this.template === 'function') {
const template = this.template();
if (typeof template === 'string') {
const parser = new DOMParser();
const rendered = parser.parseFromString(template, 'text/html');
if (rendered.body.childNodes > 1) {
throw new Error('Template must only have one root node');
}
this.reactivityMount(rendered.body.childNodes[0]);
} else if (template instanceof HTMLElement) {
this.reactivityMount(template);
}
} else if (typeof this.template === 'string') {
const parser = new DOMParser();
const rendered = parser.parseFromString(this.template, 'text/html');
if (rendered.body.childNodes > 1) {
throw new Error('Template must only have one root node');
}
this.reactivityMount(rendered.body.childNodes[0]);
} else if (this.template instanceof HTMLElement) {
this.reactivityMount(this.template);
}
function reactivityInitialize() {
const mappable = reactivityGetProperties.call(this);
this.$data = reactive(reactivityCreateStore.call(this, mappable));
reactivityMapProperties.call(this, mappable);
reactivityTemplate.call(this);
}

/**
* Mount reactivity to the DOM.
* Constructor for the Reactivity functionality.
*
* Finally, the reactivity will be mounted to the DOM on the given element (or template), and will
* then be created as an application in Vue. It will then be ready for use.
* This creates the necessary properties and modifies the constructor to initialize the Reactivity
* functionality.
*/
function reactivityMount(element, parent = null) {
// Do not re-enable reactivity
if (this.reactivityInitialized) {
throw new Error('Reactivity already initialized for this instance');
}

let parentNode = parent;

if (document.body.contains(element) && !parent) {
parentNode = element.parentNode;
} else if (!document.body.contains(element) && !parent) {
parentNode = document.body;
}
function reactivityConstructor() {
this.$reactive = false;
this.$data = {};
this.$el = null;

if (!document.body.contains(element)) {
parentNode.appendChild(element);
// Wrap the instance's constructor to call the Reactivity initialisation after construct
if (typeof this.construct === 'function') {
this.baseConstruct = this.construct;
this.construct = (...args) => {
this.baseConstruct(...args);
reactivityInitialize.call(this);
};
}

createApp(this.reactivityStore).mount(element);
this.reactivityElement = element;

// Prevent reactivity from being reset
this.reactivityInitialized = true;
Object.defineProperty(this, 'reactivityInitialized', { configurable: false, writable: false });
}

export {
reactivityConstructor,
reactivityInitialize,
reactivityGetProperties,
reactivityCreateStore,
reactivityMapProperties,
reactivityTemplate,
reactivityMount,
};
Loading

0 comments on commit d296919

Please sign in to comment.