Simple observed object reactivity
This commit is contained in:
@@ -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() {
|
||||
// class Form extends ObservedObject {
|
||||
// id
|
||||
|
||||
39
Test/params.test.js
Normal file
39
Test/params.test.js
Normal file
@@ -0,0 +1,39 @@
|
||||
window.testSuites.push( class testParams {
|
||||
|
||||
Args() {
|
||||
|
||||
}
|
||||
|
||||
Constructor() {
|
||||
|
||||
}
|
||||
|
||||
Default() {
|
||||
|
||||
}
|
||||
|
||||
// 2/3
|
||||
|
||||
DefaultArgs() {
|
||||
|
||||
}
|
||||
|
||||
ConstructorArgs() {
|
||||
|
||||
}
|
||||
|
||||
ConstructorDefault() {
|
||||
|
||||
}
|
||||
|
||||
// 3/3
|
||||
|
||||
ConstructorDefaultArgs() {
|
||||
|
||||
}
|
||||
|
||||
ConstructorDefault() {
|
||||
|
||||
}
|
||||
|
||||
})
|
||||
@@ -5,7 +5,6 @@ window.testSuites.push( class testRender {
|
||||
$form
|
||||
|
||||
render = () => {
|
||||
console.log("render", window.rendering)
|
||||
p(this.form.data)
|
||||
}
|
||||
|
||||
@@ -31,7 +30,6 @@ window.testSuites.push( class testRender {
|
||||
$name
|
||||
|
||||
render = () => {
|
||||
console.log("render", window.rendering)
|
||||
p(this.name)
|
||||
}
|
||||
|
||||
@@ -55,7 +53,6 @@ window.testSuites.push( class testRender {
|
||||
$name = "asd"
|
||||
|
||||
render = () => {
|
||||
console.log("render", window.rendering)
|
||||
p(this.name)
|
||||
p("asd")
|
||||
}
|
||||
@@ -69,14 +66,100 @@ window.testSuites.push( class testRender {
|
||||
const file = File()
|
||||
file.name = "hey123"
|
||||
|
||||
let childs = Array.from(file.children)
|
||||
childs.forEach((el) => {
|
||||
console.log(el.innerText)
|
||||
})
|
||||
|
||||
if(file.children[1].innerText === "hey123") {
|
||||
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"
|
||||
// }
|
||||
// }
|
||||
|
||||
})
|
||||
@@ -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 {
|
||||
|
||||
render = () => {
|
||||
@@ -133,21 +157,21 @@ window.testSuites.push( class testShadow {
|
||||
|
||||
constructor() {
|
||||
super()
|
||||
this.hey = "unallowed"
|
||||
}
|
||||
}, randomName("file"))
|
||||
|
||||
try {
|
||||
const file = File()
|
||||
file.hey = "unallowed"
|
||||
return "Did not throw error!"
|
||||
} catch(e) {
|
||||
if(!e.message.includes("Extensible")) {
|
||||
if(!e.message.includes("extensible")) {
|
||||
throw e
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SetNonStateFields() {
|
||||
NonStateFieldsGetSet() {
|
||||
register(class File extends Shadow {
|
||||
nonStateField
|
||||
|
||||
@@ -157,9 +181,29 @@ window.testSuites.push( class testShadow {
|
||||
}, randomName("file"))
|
||||
|
||||
const file = File("asd")
|
||||
if(!file.nonStateField === "asd") {
|
||||
if(!(file.nonStateField === "asd")) {
|
||||
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
363
index.js
@@ -145,13 +145,39 @@ function getSafariVersion() {
|
||||
|
||||
/* REGISTER */
|
||||
class ObservedObject {
|
||||
constructor() {
|
||||
this._observers = {}
|
||||
}
|
||||
|
||||
static decode(obj) {
|
||||
let instance = new this()
|
||||
|
||||
Object.keys(instance).forEach((key) => {
|
||||
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]) {
|
||||
instance[key] = obj[key]
|
||||
} else {
|
||||
@@ -160,6 +186,7 @@ class ObservedObject {
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return instance
|
||||
}
|
||||
}
|
||||
@@ -178,6 +205,200 @@ window.Shadow = class Shadow extends HTMLElement {
|
||||
|
||||
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) {
|
||||
let str = classObject.toString();
|
||||
const lines = str.split('\n');
|
||||
@@ -243,7 +464,6 @@ window.Registry = class Registry {
|
||||
|
||||
if(superCallFound) {
|
||||
let modifiedStr = modifiedLines.join('\n');
|
||||
console.log(modifiedStr)
|
||||
return eval('(' + modifiedStr + ')');
|
||||
}
|
||||
|
||||
@@ -261,134 +481,6 @@ window.Registry = class Registry {
|
||||
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 */
|
||||
@@ -411,16 +503,9 @@ window.img = function img({width="", height="", src=""}) {
|
||||
}
|
||||
|
||||
window.p = function p(innerText) {
|
||||
let parent = window.rendering[window.rendering.length-1]
|
||||
let para = document.createElement("p")
|
||||
para.innerText = innerText
|
||||
if(Registry.lastState[0] && parent[Registry.lastState[0]] === 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.initReactivity(para, "innerText", innerText)
|
||||
Registry.render(para)
|
||||
return para
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user