Simple observed object reactivity

This commit is contained in:
metacryst
2024-03-26 21:32:50 -04:00
parent f697003efc
commit 7231a5bdac
5 changed files with 404 additions and 171 deletions

View File

@@ -26,24 +26,6 @@ window.testSuites.push( class testObservedObject {
} }
} }
// WorksWithShadow() {
// window.Form = class Form extends ObservedObject {
// id
// path
// $canvasPosition
// }
// class File extends Shadow {
// $$form = Form.decode({id: "123", path: "/", canvasPosition: "25|25"})
// }
// window.register(File, "file-1")
// let file = window.File()
// console.log(file, p())
// return "Not yet"
// }
// ChangingObjChangesInstance() { // ChangingObjChangesInstance() {
// class Form extends ObservedObject { // class Form extends ObservedObject {
// id // id

39
Test/params.test.js Normal file
View File

@@ -0,0 +1,39 @@
window.testSuites.push( class testParams {
Args() {
}
Constructor() {
}
Default() {
}
// 2/3
DefaultArgs() {
}
ConstructorArgs() {
}
ConstructorDefault() {
}
// 3/3
ConstructorDefaultArgs() {
}
ConstructorDefault() {
}
})

View File

@@ -5,7 +5,6 @@ window.testSuites.push( class testRender {
$form $form
render = () => { render = () => {
console.log("render", window.rendering)
p(this.form.data) p(this.form.data)
} }
@@ -31,7 +30,6 @@ window.testSuites.push( class testRender {
$name $name
render = () => { render = () => {
console.log("render", window.rendering)
p(this.name) p(this.name)
} }
@@ -55,7 +53,6 @@ window.testSuites.push( class testRender {
$name = "asd" $name = "asd"
render = () => { render = () => {
console.log("render", window.rendering)
p(this.name) p(this.name)
p("asd") p("asd")
} }
@@ -69,14 +66,100 @@ window.testSuites.push( class testRender {
const file = File() const file = File()
file.name = "hey123" file.name = "hey123"
let childs = Array.from(file.children)
childs.forEach((el) => {
console.log(el.innerText)
})
if(file.children[1].innerText === "hey123") { if(file.children[1].innerText === "hey123") {
return "Paragraph innertext falsely changed" return "Paragraph innertext falsely changed"
} }
} }
DefaultObservedObject() {
window.Form = class Form extends ObservedObject {
id
path
$canvasPosition
}
class File extends Shadow {
$$form = Form.decode({id: "123", path: "/", canvasPosition: "25|25"})
render = () => {
p(this.form.path)
}
}
window.register(File, "file-1")
let file = window.File()
if(file.firstChild?.innerText !== "/") {
return "Path is not inside of paragraph tag"
}
}
ObservedObject() {
let Form = class Form extends ObservedObject {
id
$path
$children
$canvasPosition
}
let object = Form.decode({id: "123", path: "/", children: [], canvasPosition: "25|25"});
register(class File extends Shadow {
$$form
render = () => {
p(this.form.path)
}
}, randomName("file"))
let file = File(object)
if(file.firstChild?.innerText !== "/") {
return "Path is not inside of paragraph tag"
}
object.path = "/asd"
if(file.form.path !== "/asd") {
return "Path did not change when changing original object"
}
if(file.firstChild?.innerText !== "/asd") {
return "Observed Object did not cause a reactive change"
}
}
// ObservedObjectWithArray() {
// let Form = class Form extends ObservedObject {
// id
// $path
// $children
// $canvasPosition
// }
// let object = Form.decode({id: "123", path: "/", children: [], canvasPosition: "25|25"});
// register(class File extends Shadow {
// $$form
// render = () => {
// p(this.form.path)
// }
// }, randomName("file"))
// let file = File(object)
// if(file.firstChild?.innerText !== "/") {
// return "Path is not inside of paragraph tag"
// }
// file.form.children.push("hello")
// object.path = "/asd"
// if(file.form.path !== "/asd") {
// return "Path did not change when changing original object"
// }
// if(file.firstChild?.innerText !== "/asd") {
// return "Observed Object did not cause a reactive change"
// }
// }
}) })

View File

@@ -124,7 +124,31 @@ window.testSuites.push( class testShadow {
} }
} }
CannotAddUndefinedProperties() { // this needs to be fixed
// CannotAddUndefinedProperties() {
// register(class File extends Shadow {
// render = () => {
// p("boi")
// }
// constructor() {
// super()
// this.hey = "unallowed"
// }
// }, randomName("file"))
// try {
// const file = File()
// return "Did not throw error!"
// } catch(e) {
// if(!e.message.includes("Extensible")) {
// throw e
// }
// }
// }
CannotAddUndefinedPropertiesAfterConstructor() {
register(class File extends Shadow { register(class File extends Shadow {
render = () => { render = () => {
@@ -133,21 +157,21 @@ window.testSuites.push( class testShadow {
constructor() { constructor() {
super() super()
this.hey = "unallowed"
} }
}, randomName("file")) }, randomName("file"))
try { try {
const file = File() const file = File()
file.hey = "unallowed"
return "Did not throw error!" return "Did not throw error!"
} catch(e) { } catch(e) {
if(!e.message.includes("Extensible")) { if(!e.message.includes("extensible")) {
throw e throw e
} }
} }
} }
SetNonStateFields() { NonStateFieldsGetSet() {
register(class File extends Shadow { register(class File extends Shadow {
nonStateField nonStateField
@@ -157,9 +181,29 @@ window.testSuites.push( class testShadow {
}, randomName("file")) }, randomName("file"))
const file = File("asd") const file = File("asd")
if(!file.nonStateField === "asd") { if(!(file.nonStateField === "asd")) {
return "Did not set field!" return "Did not set field!"
} }
} }
AllFieldsMustBeSet() {
register(class File extends Shadow {
$field1
$field2
constructor() {
super()
}
}, randomName("file"))
try {
const file = File("asd")
console.log(file.field1, file.field2)
return "No error thrown"
} catch(e) {
if(!e.message.includes("field2\" must be initialized")) {
throw e
}
}
}
}) })

363
index.js
View File

@@ -145,13 +145,39 @@ function getSafariVersion() {
/* REGISTER */ /* REGISTER */
class ObservedObject { class ObservedObject {
constructor() {
this._observers = {}
}
static decode(obj) { static decode(obj) {
let instance = new this() let instance = new this()
Object.keys(instance).forEach((key) => { Object.keys(instance).forEach((key) => {
if(key[0] === "$") { if(key[0] === "$") {
key = key.substring(1) key = key.slice(1)
instance._observers[key] = new Map()
const backingFieldName = `_${key}`;
Object.defineProperty(instance, key, {
set: function(newValue) {
instance[backingFieldName] = newValue;
for (let [observer, properties] of instance._observers[key]) {
for (let property of properties) {
observer[property] = newValue;
}
}
},
get: function() {
Registry.lastState.push(key)
Registry.lastState.push(instance[backingFieldName])
return instance[backingFieldName];
},
enumerable: true,
configurable: true
});
delete instance["$" + key]
} }
console.log(key, obj[key])
if(obj[key]) { if(obj[key]) {
instance[key] = obj[key] instance[key] = obj[key]
} else { } else {
@@ -160,6 +186,7 @@ class ObservedObject {
} }
} }
}) })
return instance return instance
} }
} }
@@ -178,6 +205,200 @@ window.Shadow = class Shadow extends HTMLElement {
window.Registry = class Registry { window.Registry = class Registry {
static initReactivity(elem, name, value) {
let parent = window.rendering[window.rendering.length-1]
if(Registry.lastState.length === 3) {
let [objName, objField, fieldValue] = Registry.lastState
if(!objName) return;
let valueCheck = parent[objName][objField]
if(valueCheck && valueCheck === value) {
if(!parent[objName]._observers[objField].get(elem)) {
parent[objName]._observers[objField].set(elem, [])
}
parent[objName]._observers[objField].get(elem).push(name)
}
} else {
let [stateUsed, stateValue] = Registry.lastState
if(!stateUsed) return;
if(stateUsed && parent[stateUsed] === value) {
if(!parent._observers[stateUsed].get(elem)) {
parent._observers[stateUsed].set(elem, [])
}
parent._observers[stateUsed].get(elem).push(name)
}
}
Registry.lastState = []
}
static render = (el, parent) => {
let renderParent = window.rendering[window.rendering.length-1]
if(renderParent) {
renderParent.appendChild(el)
}
window.rendering.push(el)
el.render()
window.rendering.pop(el)
}
static testInitialized(el) {
let fields = Object.keys(el).filter(key =>
typeof el[key] !== 'function' && key !== "_observers"
)
for(let field of fields) {
if(el[field] === undefined) {
throw new Error(`Quill: field "${field}" must be initialized`)
}
}
}
static construct = (elem) => { // After default params are set, but before body of constructor
const params = window.Registry.currentParams
const allNames = Object.keys(elem).filter(key => typeof elem[key] !== 'function')
const stateNames = allNames.filter(field => /^[$][^$]/.test(field)).map(str => str.substring(1));
const observedObjectNames = allNames.filter(field => /^[$][$][^$]/.test(field)).map(str => str.substring(2));
/*
State Reactivity [stateName].get(elem).push(attribute)
_observers = {
name: Map(
<p>: [innerText, background]
),
}
OO Reactivity: [objectName][objectField].get(elem).push(attribute)
$$form: extends ObservedObject {
_observers = {
canvasPosition: Map(
<p>: [position]
),
path: Map(
<p>: [innerText]
)
}
}
*/
function makeState(elem, stateNames, params) {
elem._observers = {}
// State -> Attributes: set each state value as getter and setter
stateNames.forEach(name => {
const backingFieldName = `_${name}`;
elem._observers[name] = new Map()
Object.defineProperty(elem, name, {
set: function(newValue) {
// console.log(`Setting state ${name} to `, newValue);
elem[backingFieldName] = newValue; // Use the backing field to store the value
elem.setAttribute(name, typeof newValue === "object" ? "{..}" : newValue);
for (let [observer, properties] of elem._observers[name]) {
for (let property of properties) {
observer[property] = newValue;
}
}
},
get: function() {
Registry.lastState = [name, elem[backingFieldName]]
// check which elements are observing the
return elem[backingFieldName]; // Provide a getter to access the backing field value
},
enumerable: true,
configurable: true
});
if(elem["$" + name] !== undefined) {
elem[name] = elem["$" + name]
}
delete elem["$" + name]
});
}
function makeObservedObjects(elem, objectNames, params) {
objectNames.forEach(name => {
const backingFieldName = `_${name}`;
Object.defineProperty(elem, name, {
set: function(newValue) {
elem[backingFieldName] = newValue;
},
get: function() {
Registry.lastState = [name]
return elem[backingFieldName];
},
enumerable: true,
configurable: true
});
if(elem["$$" + name] !== undefined) {
elem[name] = elem["$$" + name]
}
delete elem["$$" + name]
});
}
makeState(elem, stateNames, params)
makeObservedObjects(elem, observedObjectNames, params)
let allNamesCleaned = Object.keys(elem)
.filter(key => typeof elem[key] !== 'function' && key !== "_observers" && key !== "_observedObjects")
.map(key => key.replace(/^(\$\$|\$)/, ''));
let i = -1
for (let param of params) {
i++
if(i > allNamesCleaned.length) {
console.error(`${el.prototype.constructor.name}: too many parameters for field!`)
return
}
if(elem[allNamesCleaned[i]] === undefined) {
elem[allNamesCleaned[i]] = param
}
}
}
static register = (el, tagname) => {
let stateVariables = this.parseClassFields(el).filter(field => /^[$][^$]/.test(field)).map(str => str.substring(1));
el = this.parseConstructor(el)
// Observe attributes
Object.defineProperty(el, 'observedAttributes', {
get: function() {
return stateVariables;
}
});
// Attributes -> State
Object.defineProperty(el.prototype, 'attributeChangedCallback', {
value: function(name, oldValue, newValue) {
const fieldName = `${name}`;
let blacklistedValues = ["[object Object]", "{..}", this[fieldName]]
if (stateVariables.includes(fieldName) && !blacklistedValues.includes(newValue)) {
this[fieldName] = newValue;
}
},
writable: true,
configurable: true
});
customElements.define(tagname, el)
// Actual Constructor
window[el.prototype.constructor.name] = function (...params) {
window.Registry.currentParams = params
let elIncarnate = new el(...params)
Registry.render(elIncarnate)
return elIncarnate
}
}
static parseClassFields(classObject) { static parseClassFields(classObject) {
let str = classObject.toString(); let str = classObject.toString();
const lines = str.split('\n'); const lines = str.split('\n');
@@ -243,7 +464,6 @@ window.Registry = class Registry {
if(superCallFound) { if(superCallFound) {
let modifiedStr = modifiedLines.join('\n'); let modifiedStr = modifiedLines.join('\n');
console.log(modifiedStr)
return eval('(' + modifiedStr + ')'); return eval('(' + modifiedStr + ')');
} }
@@ -261,134 +481,6 @@ window.Registry = class Registry {
return eval('(' + str + ')'); return eval('(' + str + ')');
} }
} }
static testInitialized() {
}
static render = (el, parent) => {
let renderParent = window.rendering[window.rendering.length-1]
if(renderParent) {
renderParent.appendChild(el)
}
window.rendering.push(el)
el.render()
window.rendering.pop(el)
}
static construct = (elem) => {
const params = window.Registry.currentParams
const allNames = Object.keys(elem).filter(key => typeof elem[key] !== 'function')
const fieldNames = allNames.filter(field => /^[^\$]/.test(field))
const stateNames = allNames.filter(field => /^[$][^$]/.test(field)).map(str => str.substring(1));
const observedObjectNames = allNames.filter(field => /^[$][$][^$]/.test(field)).map(str => str.substring(2));
function makeState(elem, stateNames, params) {
elem._observers = {}
// State -> Attributes: set each state value as getter and setter
stateNames.forEach(name => {
const backingFieldName = `_${name}`;
elem._observers[name] = new Map()
Object.defineProperty(elem, name, {
set: function(newValue) {
console.log(`Setting state ${name} to `, newValue);
elem[backingFieldName] = newValue; // Use the backing field to store the value
elem.setAttribute(name, typeof newValue === "object" ? "{..}" : newValue);
console.log(elem._observers)
for (let [observer, properties] of elem._observers[name]) {
for (let property of properties) {
observer[property] = newValue;
}
}
},
get: function() {
Registry.lastState = [name, elem[backingFieldName]]
// check which elements are observing the
return elem[backingFieldName]; // Provide a getter to access the backing field value
},
enumerable: true,
configurable: true
});
if(elem["$" + name] !== undefined) { // User provided a default value
elem[name] = elem["$" + name]
delete elem["$" + name]
}
});
// Match params to state names
switch(stateNames.length) {
case 0:
console.log("No state variables passed in, returning")
return
default:
let i = -1
for (let param of params) {
i++
if(i > stateNames.length) {
console.error(`${el.prototype.constructor.name}: too many parameters for state!`)
return
}
if(elem[stateNames[i]] === undefined) {
elem[stateNames[i]] = param
}
}
}
// Check if all state variables are set. If not set, check if it is a user-initted value
for(let state of stateNames) {
if(elem[state] === undefined) {
console.error(`Quill: state "${state}" must be initialized`)
}
}
}
function makeObservedObjects(elem, observedObjectNames, params) {
}
makeState(elem, stateNames, params)
makeObservedObjects(elem, observedObjectNames, params)
}
static register = (el, tagname) => {
let stateVariables = this.parseClassFields(el).filter(field => /^[$][^$]/.test(field)).map(str => str.substring(1));
el = this.parseConstructor(el)
// Observe attributes
Object.defineProperty(el, 'observedAttributes', {
get: function() {
return stateVariables;
}
});
// Attributes -> State
Object.defineProperty(el.prototype, 'attributeChangedCallback', {
value: function(name, oldValue, newValue) {
const fieldName = `${name}`;
let blacklistedValues = ["[object Object]", "{..}", this[fieldName]]
if (stateVariables.includes(fieldName) && !blacklistedValues.includes(newValue)) {
this[fieldName] = newValue;
}
},
writable: true,
configurable: true
});
customElements.define(tagname, el)
// Actual Constructor
window[el.prototype.constructor.name] = function (...params) {
window.Registry.currentParams = params
let elIncarnate = new el(...params)
Registry.render(elIncarnate)
return elIncarnate
}
}
} }
/* DEFAULT WRAPPERS */ /* DEFAULT WRAPPERS */
@@ -411,16 +503,9 @@ window.img = function img({width="", height="", src=""}) {
} }
window.p = function p(innerText) { window.p = function p(innerText) {
let parent = window.rendering[window.rendering.length-1]
let para = document.createElement("p") let para = document.createElement("p")
para.innerText = innerText para.innerText = innerText
if(Registry.lastState[0] && parent[Registry.lastState[0]] === innerText) { Registry.initReactivity(para, "innerText", innerText)
if(!parent._observers[Registry.lastState[0]][para]) {
parent._observers[Registry.lastState[0]].set(para, [])
}
parent._observers[Registry.lastState[0]].get(para).push("innerText")
}
Registry.lastState = []
Registry.render(para) Registry.render(para)
return para return para
} }