diff --git a/Test/observedobject.test.js b/Test/observedobject.test.js new file mode 100644 index 0000000..33d5cbf --- /dev/null +++ b/Test/observedobject.test.js @@ -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!" + // } + // } + +}) \ No newline at end of file diff --git a/Test/render.test.js b/Test/render.test.js new file mode 100644 index 0000000..a198c75 --- /dev/null +++ b/Test/render.test.js @@ -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" + } + } + +}) \ No newline at end of file diff --git a/Test/shadow.test.js b/Test/shadow.test.js index 5a39c37..a18297b 100644 --- a/Test/shadow.test.js +++ b/Test/shadow.test.js @@ -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!" } }) \ No newline at end of file diff --git a/Test/test.js b/Test/test.js index 5d0e6b1..dac3514 100644 --- a/Test/test.js +++ b/Test/test.js @@ -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 diff --git a/index.js b/index.js index c672dc3..1e94cf6 100644 --- a/index.js +++ b/index.js @@ -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 + + if (trimmedLine.startsWith('constructor(')) { + 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,62 +276,87 @@ window.Registry = class Registry { window.rendering.pop(el) } - static construct = (elem, stateNames, ...params) => { - // State -> Attributes: set each state value as getter and setter - stateNames.forEach(name => { - const backingFieldName = `_${name}`; - - Object.defineProperty(elem, name, { - set: function(newValue) { - // console.log(`Setting attribute ${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 - }, - get: function() { - // console.log("get: ", elem[backingFieldName]) - return elem[backingFieldName]; // Provide a getter to access the backing field value - }, - enumerable: true, - configurable: true + 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] + } }); - 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 + } - // 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 + } } + } - 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`) } } } - // 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 => 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) { - this.innerHTML = "" - if(els) { + if(els.length > 0) { + this.innerHTML = "" els.forEach((el) => { this.appendChild(el) })