Reactivity for state

This commit is contained in:
metacryst
2024-03-24 22:30:29 +01:00
parent f0d04d9f0d
commit f697003efc
5 changed files with 329 additions and 75 deletions

View File

@@ -0,0 +1,62 @@
window.testSuites.push( class testObservedObject {
FromJSONFailsWithoutAllFields() {
class Form extends ObservedObject {
id
path
$canvasPosition
}
try {
let obj = Form.decode({id: "123"})
return "Not implemented"
} catch {}
}
FromJSONInitsAllFields() {
class Form extends ObservedObject {
id
path
$canvasPosition
}
let obj = Form.decode({id: "123", path: "/", canvasPosition: "25|25"})
if(!(obj && obj["id"] === "123" && obj["path"] === "/" && obj["canvasPosition"] === "25|25")) {
return "Not all fields initialized!"
}
}
// 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
// path
// $canvasPosition
// }
// let json = {id: "123", path: "/", canvasPosition: "25|25"}
// let obj = Form.decode({id: "123", path: "/", canvasPosition: "25|25"})
// json.id = "456"
// if(!(obj["id"] === "456")) {
// return "Change to original object was not reflected!"
// }
// }
})

82
Test/render.test.js Normal file
View File

@@ -0,0 +1,82 @@
window.testSuites.push( class testRender {
SimpleParagraphWithState() {
class File extends Shadow {
$form
render = () => {
console.log("render", window.rendering)
p(this.form.data)
}
constructor() {
super()
}
}
window.register(File, randomName("file"))
let form = {data: "asdf"}
const el = window.File(form)
if(!(el.firstChild?.matches("p"))) {
return `Child paragraph not rendered`
}
if(!(el.firstChild.innerText === "asdf")) {
return "Child para does not have inner text"
}
}
ParagraphConstructorChangeState() {
register(class File extends Shadow {
$name
render = () => {
console.log("render", window.rendering)
p(this.name)
}
constructor() {
super()
}
}, randomName("file"))
let name = "asdf"
const file = File(name)
file.name = "hey123"
if(file.firstChild.innerText !== "hey123") {
return "Paragraph did not react to change!"
}
}
LiteralDoesNotCreateFalseReactivity() {
register(class File extends Shadow {
$name = "asd"
render = () => {
console.log("render", window.rendering)
p(this.name)
p("asd")
}
constructor() {
super()
}
}, randomName("file"))
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"
}
}
})

View File

@@ -124,22 +124,42 @@ window.testSuites.push( class testShadow {
}
}
RegisterThrowsIfNoConstructorParams() {
class File3 extends Shadow {
$form
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
}
}
}
SetNonStateFields() {
register(class File extends Shadow {
nonStateField
constructor() {
super()
}
}
}, randomName("file"))
try {
window.register(File3, "file3-el")
} catch(e) {
return
const file = File("asd")
if(!file.nonStateField === "asd") {
return "Did not set field!"
}
return "Error not thrown!"
}
})

View File

@@ -3,6 +3,23 @@ window.testSuites = [];
await import ("./parse.test.js")
await import ("./shadow.test.js")
await import ("./observedobject.test.js")
await import ("./render.test.js")
window.randomName = function randomName(prefix) {
const sanitizedPrefix = prefix.toLowerCase().replace(/[^a-z0-9]/g, '');
// Generate a random suffix using numbers and lowercase letters
const suffixLength = 8; // You can adjust the length of the suffix
const suffixChars = 'abcdefghijklmnopqrstuvwxyz0123456789';
let suffix = '';
for (let i = 0; i < suffixLength; i++) {
suffix += suffixChars.charAt(Math.floor(Math.random() * suffixChars.length));
}
// Combine the prefix and suffix with a hyphen
return `${sanitizedPrefix}-${suffix}`;
}
window.test = async function() {
// window.testSuites.sort();
@@ -24,7 +41,15 @@ window.test = async function() {
if(typeof suite[test] === 'function' && test !== "constructor") {
testNum++;
console.log(`%c${testNum}. ${test}`, "margin-top: 10px; border-top: 2px solid #e9c9a0; color: #e9c9a0; border-radius: 10px; padding: 10px;");
let fail = await suite[test]();
let fail;
try {
fail = await suite[test]()
} catch(e) {
console.error(e)
fail = "Error"
}
if(fail) {
failed++;
let spaceNum = test.length - fail.length

109
index.js
View File

@@ -144,12 +144,24 @@ function getSafariVersion() {
}
/* REGISTER */
window.ObservedObject = class ObservedObject {
//
// "$" Variable: triggers a UI change
// Array: triggers a change if something is added, deleted, changed at the top level
//
class ObservedObject {
static decode(obj) {
let instance = new this()
Object.keys(instance).forEach((key) => {
if(key[0] === "$") {
key = key.substring(1)
}
console.log(key, obj[key])
if(obj[key]) {
instance[key] = obj[key]
} else {
if(!instance[key]) {
throw new Error(`ObservedObject: Non-default value "${key}" must be initialized!`)
}
}
})
return instance
}
}
window.Page = class Page {
@@ -200,27 +212,39 @@ window.Registry = class Registry {
static parseConstructor(classObject) {
let str = classObject.toString();
const lines = str.split('\n');
let modifiedLines = [];
let braceDepth = 0;
let constructorFound = false
let superCallFound = false;
for (let line of lines) {
for (let i = 0; i < lines.length; i++) {
let line = lines[i];
const trimmedLine = line.trim();
modifiedLines.push(line);
braceDepth += (trimmedLine.match(/{/g) || []).length;
braceDepth -= (trimmedLine.match(/}/g) || []).length;
if (trimmedLine.startsWith('constructor(')) {
constructorFound = true
constructorFound = true;
}
if (braceDepth === 2) {
if (trimmedLine.startsWith('super(')) {
constructorFound = true
var newLine = trimmedLine + "\nwindow.Registry.construct(this, window.Registry.currentStateVariables, ...window.Registry.currentParams)\n";
str = str.replace(line, newLine)
return eval('(' + str + ')');
if (constructorFound && trimmedLine.startsWith('super(') && !superCallFound) {
superCallFound = true;
modifiedLines.push(` window.Registry.construct(this, window.Registry.currentStateVariables, ...window.Registry.currentParams);`);
}
if (constructorFound && braceDepth === 1 && superCallFound) {
modifiedLines.splice(modifiedLines.length - 1, 0, ' Object.preventExtensions(this);');
modifiedLines.splice(modifiedLines.length - 1, 0, ' window.Registry.testInitialized(this);');
}
}
if(superCallFound) {
let modifiedStr = modifiedLines.join('\n');
console.log(modifiedStr)
return eval('(' + modifiedStr + ')');
}
if(constructorFound) {
@@ -229,7 +253,7 @@ window.Registry = class Registry {
let constructorString = `
constructor(...params) {
super(...params)
window.Registry.construct(this, window.Registry.currentStateVariables, ...window.Registry.currentParams)
window.Registry.construct(this)
}
`
let closingBracket = str.lastIndexOf("}");
@@ -238,6 +262,10 @@ window.Registry = class Registry {
}
}
static testInitialized() {
}
static render = (el, parent) => {
let renderParent = window.rendering[window.rendering.length-1]
if(renderParent) {
@@ -248,19 +276,36 @@ window.Registry = class Registry {
window.rendering.pop(el)
}
static construct = (elem, stateNames, ...params) => {
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 attribute ${name} to `, 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); // Synchronize with the attribute
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() {
// console.log("get: ", elem[backingFieldName])
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,
@@ -302,8 +347,16 @@ window.Registry = class Registry {
}
}
function makeObservedObjects(elem, observedObjectNames, params) {
}
makeState(elem, stateNames, params)
makeObservedObjects(elem, observedObjectNames, params)
}
static register = (el, tagname) => {
let stateVariables = this.parseClassFields(el).filter(field => field.startsWith('$')).map(str => str.substring(1));
let stateVariables = this.parseClassFields(el).filter(field => /^[$][^$]/.test(field)).map(str => str.substring(1));
el = this.parseConstructor(el)
// Observe attributes
@@ -330,7 +383,6 @@ window.Registry = class Registry {
// Actual Constructor
window[el.prototype.constructor.name] = function (...params) {
window.Registry.currentStateVariables = stateVariables
window.Registry.currentParams = params
let elIncarnate = new el(...params)
Registry.render(elIncarnate)
@@ -345,6 +397,7 @@ window.a = function a({ href, name=href } = {}) {
let link = document.createElement("a")
link.setAttribute('href', href);
link.innerText = name
Registry.render(link)
return link
}
@@ -353,24 +406,36 @@ window.img = function img({width="", height="", src=""}) {
if(width) image.style.width = width
if(height) image.style.height = height
if(src) image.src = src
Registry.render(image)
return image
}
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.render(para)
return para
}
window.div = function (innerText) {
let div = document.createElement("div")
div.innerText = innerText
Registry.render(div)
return div
}
window.span = function (innerText) {
let span = document.createElement("span")
span.innerText = innerText
Registry.render(span)
return span
}
@@ -401,8 +466,8 @@ HTMLElement.prototype.endingTag = function() {
}
HTMLElement.prototype.render = function (...els) {
if(els.length > 0) {
this.innerHTML = ""
if(els) {
els.forEach((el) => {
this.appendChild(el)
})