From 41ec6b7dd37c5f8028167ce13908062696c44a95 Mon Sep 17 00:00:00 2001 From: metacryst Date: Fri, 17 May 2024 12:47:17 -0500 Subject: [PATCH] ternaries almost done, adding margin function --- README.md | 3 + Test/observedobject.test.js | 85 +++++++++++++++++++ Test/parserender.test.js | 18 ++-- Test/render.test.js | 159 ------------------------------------ Test/stacks.test.js | 41 ++++++++++ Test/state.test.js | 102 +++++++++++++++++++++++ Test/test.js | 3 +- index.js | 128 +++++++++++++++++++++++------ 8 files changed, 350 insertions(+), 189 deletions(-) delete mode 100644 Test/render.test.js create mode 100644 Test/stacks.test.js create mode 100644 Test/state.test.js diff --git a/README.md b/README.md index 08ffbda..a588646 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,9 @@ document.body.append( Ternaries within render() Other statements within render() +## Limitations: +While rendering is underway, an element's state can only be accessed from within that element + ## Boilerplate: - ```*html```: Type in an HTML file and select the suggestion to create HTML boilerplate. - ```*element```: Type in a JS file and select the suggestion to create JS Custom Element boilerplate. diff --git a/Test/observedobject.test.js b/Test/observedobject.test.js index c9a6288..ab1e4b8 100644 --- a/Test/observedobject.test.js +++ b/Test/observedobject.test.js @@ -41,6 +41,91 @@ window.testSuites.push( class testObservedObject { // throw some sort of warning if a global OO is accessed without "this" + DefaultObservedObject() { + window.Form = class Form extends ObservedObject { + id + path + $canvasPosition + } + + class File extends Shadow { + $$form = Form.create({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 + $canvasPosition + } + + let object = Form.create({id: "123", path: "/", 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 + $children + } + + let object = Form.create({id: "123", children: [{path: "berry"}, {path: "blue"}]}); + + register(class File extends Shadow { + $$form + + render = () => { + ForEach(this.form.children, (child) => { + p(child.path) + }) + } + }, randomName("file")) + + let file = File(object) + + if(file.firstChild?.innerText !== "berry" || file.children[1].innerText !== "blue") { + return "Paths did not render correctly in children" + } + + file.form.children.push({path: "hello"}) + if(file.children.length !== 3) { + return "No reactivity for adding children" + } + } + NotExtensible() { return "not done" } diff --git a/Test/parserender.test.js b/Test/parserender.test.js index 80f52c8..96f3cce 100644 --- a/Test/parserender.test.js +++ b/Test/parserender.test.js @@ -1,13 +1,19 @@ + +/* + +"(" is preceding character: el, el.attr, if, switch + el: the el is window.rendering + el.attr: + find the function and attr in the string + if there are multiple instances of being used with this el, add to a list (and if no list then make it and we are first) + if: the el is window.rendering. rerender el + switch: the el is window.rendering. rerender el +*/ + window.testSuites.push( class ParseRender { - // CopyTo() { - // let str = "render=()=>{VStack(()=>{" - // let ret = str.copyTo("{") - - // if(ret !== "render=()=>") return "Copy 1 failed!" - // } ParseRender() { class Sidebar extends Shadow { diff --git a/Test/render.test.js b/Test/render.test.js deleted file mode 100644 index c54a3ef..0000000 --- a/Test/render.test.js +++ /dev/null @@ -1,159 +0,0 @@ -window.testSuites.push( class testRender { - - SimpleParagraphWithState() { - class File extends Shadow { - $form - - render = () => { - 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 = () => { - 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 = () => { - p(this.name) - p("asd") - } - - constructor() { - super() - } - }, randomName("file")) - - - const file = File() - file.name = "hey123" - - 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.create({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 - $canvasPosition - } - - let object = Form.create({id: "123", path: "/", 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 - $children - } - - let object = Form.create({id: "123", children: [{path: "berry"}, {path: "blue"}]}); - - register(class File extends Shadow { - $$form - - render = () => { - ForEach(this.form.children, (child) => { - p(child.path) - }) - } - }, randomName("file")) - - let file = File(object) - - if(file.firstChild?.innerText !== "berry" || file.children[1].innerText !== "blue") { - return "Paths did not render correctly in children" - } - - file.form.children.push({path: "hello"}) - if(file.children.length !== 3) { - return "No reactivity for adding children" - } - } - -}) \ No newline at end of file diff --git a/Test/stacks.test.js b/Test/stacks.test.js new file mode 100644 index 0000000..aaf8861 --- /dev/null +++ b/Test/stacks.test.js @@ -0,0 +1,41 @@ +window.testSuites.push( class testStacks { + + NestedChildren() { + register(class File extends Shadow { + $name + + render = () => { + p(this.name) + + VStack(() => { + p("Learn to code inside of a startup") + + p("➵") + .position("absolute") + .fontSize(30) + .left("50%") + .top("50%") + .transformOrigin("center") + .transform("translate(-50%, -50%) rotate(90deg)") + .transition("all 1s") + .userSelect("none") + .onAppear((self) => { + setTimeout(() => { + self.style.top = "88%" + }, 100) + }) + }) + } + + constructor() { + super() + } + }, randomName("file")) + + const file = File("asdf") + if(file.querySelector(".VStack")?.children.length !== 2) { + return "Incorrect amount of children inside vstack!" + } + } + +}) \ No newline at end of file diff --git a/Test/state.test.js b/Test/state.test.js new file mode 100644 index 0000000..1833855 --- /dev/null +++ b/Test/state.test.js @@ -0,0 +1,102 @@ +window.testSuites.push( class testState { + + SimpleParagraphWithState() { + class File extends Shadow { + $form + + render = () => { + 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 = () => { + 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 = () => { + p(this.name) + p("asd") + } + + constructor() { + super() + } + }, randomName("file")) + + + const file = File() + file.name = "hey123" + + if(file.children[1].innerText === "hey123") { + return "Paragraph innertext falsely changed" + } + } + + /* + State itself should check if the reactivity is based on an element or a standalone expression + If standalone, handle it + If element, push the info for initReactivity to handle it + */ + + TernaryInState() { + register(class File extends Shadow { + $name + + render = () => { + p(this.name) + .fontSize(this.name === "asdf" ? 16 : 32) + } + + constructor() { + super() + } + }, randomName("file")) + + let name = "asdf" + const file = File(name) + + if(file.style.fontSize !== "16px") { + return "fail" + } + } + +}) \ No newline at end of file diff --git a/Test/test.js b/Test/test.js index 15b1048..f9f9f49 100644 --- a/Test/test.js +++ b/Test/test.js @@ -3,9 +3,10 @@ window.testSuites = []; await import ("./parse.test.js") await import ("./init.test.js") -await import ("./render.test.js") await import ("./observedobject.test.js") await import ("./parserender.test.js") +await import ("./stacks.test.js") +await import ("./state.test.js") window.randomName = function randomName(prefix) { const sanitizedPrefix = prefix.toLowerCase().replace(/[^a-z0-9]/g, ''); diff --git a/index.js b/index.js index 8375623..2bf3e57 100644 --- a/index.js +++ b/index.js @@ -287,11 +287,11 @@ window.Registry = class Registry { } */ - static initReactivity(elem, name, value) { - let parent = window.rendering.last() - if(!Registry.lastState) { + static initReactivity(elem, attr, value) { + if(!Registry.lastState || Registry.lastState.length === 0) { return } + let parent = window.rendering.last() if(Registry.lastState.length === 3) { let [objName, objField, fieldValue] = Registry.lastState @@ -303,19 +303,26 @@ window.Registry = class Registry { parent[objName]._observers[objField].set(elem, []) } let properties = parent[objName]._observers[objField].get(elem) - if(!properties.includes(name)) { - properties.push(name) + if(!properties.includes(attr)) { + properties.push(attr) } } } else { let [stateUsed, stateValue] = Registry.lastState if(!stateUsed) return; - - if(stateUsed && parent[stateUsed] === value) { + + if(parent[stateUsed] === value) { if(!parent._observers[stateUsed].get(elem)) { parent._observers[stateUsed].set(elem, []) } - parent._observers[stateUsed].get(elem).push(name) + parent._observers[stateUsed].get(elem).push(attr) + } else { + // TODO: Enable this code to get the dynamic value to be called when state changes + // console.log('not equal to the value') + // const dynamicFunction = new Function(expressionString); + // parent.getValue = dynamicFunction; + // const boundFunction = parent.getValue.bind(parent); + // let value = parent.getValue() } } Registry.lastState = [] @@ -363,6 +370,71 @@ window.Registry = class Registry { 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 catalogReactivity(elem, stateNames, observedObjectNames) { + const renderString = elem.render.toString().replace(/\s/g, ""); + + const regex = /this\.([a-zA-Z0-9_$]+)/g; + let match; + let matches = []; + + while ((match = regex.exec(renderString)) !== null) { + matches.push({index: match.index, identifier: match[1]}); + } + + let foundReactives = [] + + matches.forEach(match => { + let {index: statePos, identifier} = match; + + if (stateNames.includes(identifier) || observedObjectNames.includes(identifier)) { + let charBefore = renderString[statePos - 1]; + + if (charBefore === "(") { + function getIdentifier() { + let startIndex = statePos - 2; + let identifier = ''; + + while (startIndex >= 0 && /^[a-zA-Z0-9_$]$/.test(renderString[startIndex])) { + identifier = renderString[startIndex] + identifier; + startIndex--; + } + return identifier + } + + function getExpression(renderString, startPos) { + let endIndex = startPos; + while (endIndex < renderString.length && ![')', ','].includes(renderString[endIndex])) { + endIndex++; + } + + return renderString.substring(startPos, endIndex).trim(); + } + + function containsOperators(expression) { + const operatorRegex = /[+\-*/%=&|<>!^]/; + return operatorRegex.test(expression); + } + + let identifier = getIdentifier() + let expression = getExpression(renderString, statePos) + let operators = containsOperators(expression) + + foundReactives.push([identifier, expression, operators]) + } else { + console.log("Variable or other usage at position:", statePos); + } + + if (observedObjectNames.includes(identifier)) { + } else if (stateNames.includes(identifier)) { + } + } + }); + + elem.reactives = foundReactives + } + + catalogReactivity(elem, stateNames, observedObjectNames) + function makeState(elem, stateNames, params) { elem._observers = {} @@ -370,7 +442,7 @@ window.Registry = class Registry { stateNames.forEach(name => { const backingFieldName = `_${name}`; elem._observers[name] = new Map() - + Object.defineProperty(elem, name, { set: function(newValue) { elem[backingFieldName] = newValue; @@ -386,8 +458,12 @@ window.Registry = class Registry { } }, get: function() { - Registry.lastState = [name, elem[backingFieldName]] - return elem[backingFieldName]; + let rendering = window.rendering[window.rendering.length - 1] + let value = elem[backingFieldName] + if(!rendering) return value + + Registry.lastState = [name, value] + return value; }, enumerable: true, configurable: true @@ -588,8 +664,8 @@ window.VStack = function (cb = () => {}) { div.classList.add("VStack") div.style.display = "flex" div.style.flexDirection = "column" + div.render = cb Registry.render(div) - cb() return div } } @@ -716,6 +792,23 @@ HTMLElement.prototype.padding = function(direction, value) { return this } +HTMLElement.prototype.margin = function(direction, value) { + if(!value) { + this.style.margin = direction; + Registry.initReactivity(this, ["style", "margin"], value); + return this + } + + const directionName = `margin${direction.charAt(0).toUpperCase()}${direction.slice(1)}`; + if (typeof value === 'number') { + this.style[directionName] = `${value}px`; + } else { + this.style[directionName] = value; + } + Registry.initReactivity(this, ["style", directionName], value); + return this +} + HTMLElement.prototype.width = function(value, unit = "px") { this.style.width = value + unit Registry.initReactivity(this, ["style", "width"], value); @@ -769,17 +862,6 @@ HTMLElement.prototype.yBottom = function(value, unit = "px") { return this } -HTMLElement.prototype.margin = function(direction, value) { - const directionName = `margin${direction.charAt(0).toUpperCase()}${direction.slice(1)}`; - if (typeof value === 'number') { - this.style[directionName] = `${value}px`; - } else { - this.style[directionName] = value; - } - Registry.initReactivity(this, ["style", directionName], value); - return this -} - HTMLElement.prototype.positionType = function (value) { if(!(value === "absolute" || value === "relative" || value === "static" || value === "fixed" || value === "sticky")) { console.error("HTMLElement.overlflow: must have valid overflow value!")