diff --git a/struct composability prototype, with staticmethods/Component.mojo b/struct composability prototype, with staticmethods/Component.mojo new file mode 100644 index 0000000..c717724 --- /dev/null +++ b/struct composability prototype, with staticmethods/Component.mojo @@ -0,0 +1,27 @@ +from DomTree import * +from DomEvent import * + +struct ComponentTypes[*Ts:Component]:... + +trait Component: + @staticmethod + fn component_name()->String:... + @staticmethod + fn InitializeStates()->Capsule: ... + @staticmethod + fn DeinitializeStates(owned states:Capsule): ... + @staticmethod + fn Event(owned states:Capsule, e:DomEvent): ... + @staticmethod + fn Render(owned states:Capsule,owned props:Capsule) ->Element: ... + +trait ComponentStateless: + @staticmethod + fn Render(owned Props: Capsule)->Element:... + +@value +struct Component_T: + var instance_name:String + var component_name: String + var props: Capsule + diff --git a/struct composability prototype, with staticmethods/ComponentManager.mojo b/struct composability prototype, with staticmethods/ComponentManager.mojo new file mode 100644 index 0000000..87481b9 --- /dev/null +++ b/struct composability prototype, with staticmethods/ComponentManager.mojo @@ -0,0 +1,202 @@ +from Component import * +from DomTree import * +from python import Python +from app import m_ + +def PythonDict()->PythonObject: + var tmp_result = Python.evaluate("lambda **kwargs: kwargs") + return tmp_result(empty=True) + +@value +struct Capsule(CollectionElement): + var value: UnsafePointer[NoneType] + var type: String + var instance_name: String + var rendered: Bool + fn __init__(inout self): + self.value = UnsafePointer[NoneType]() + self.rendered = False + self.type= "default" + self.instance_name = "default" + + fn __bool__(self)->Bool: return self.type!="default" + + fn __init__[T:CollectionElement](inout self,owned arg:T): + var tmp = UnsafePointer[T].alloc(1) + initialize_pointee_move(tmp,arg) + self.value = tmp.bitcast[NoneType]() + self.rendered = False + self.type= "default" + self.instance_name = "default" + + fn __getitem__[T:AnyType](inout self)->Reference[ + T, + __mlir_attr.`1: i1`, + __lifetime_of(self) + ]: + return self.value.bitcast[T]().__refitem__() + + fn Del[T:CollectionElement](inout self): + destroy_pointee(self.value.bitcast[T]()) + self.value.free() + self.type = "default" + +@value +struct ComponentManager[*Ts:Component]: + var states: List[Capsule] + + fn __init__(inout self): + constrained[ + TsUniques[Ts](), + "Two components have the same name" + ]() + + self.states = __type_of(self.states)() + + fn RenderIntoJson[First:Bool=False]( + inout self, + inout elements: Element, + )->PythonObject: + try: + var tmp_result = Python.evaluate("lambda **kwargs: kwargs")( + tag=elements.tag, + inner=[] + ) + if elements.tag == "TextNode": + tmp_result["value"]=elements.attributes["value"] + else: + for a in elements.attributes: + tmp_result[a[]]=elements.attributes[a[]] + for i in elements.inner: + if i[].isa[Element](): + tmp_result["inner"].append( + self.RenderIntoJson( + i[]._get_ptr[Element]()[] + ) + ) + @parameter + if First: return Python.import_module("json").dumps(tmp_result) + else: return tmp_result + except e: print("RenderToJson",e) + return None + + fn RenderComponentIntoElements[first:Bool,T:Component]( + inout self, + inout arg: Element + ): + var tmp = arg.component.value()[] + self.TsRenderInto[T](arg,tmp) + + + fn CreateInstance[T:Component]( + inout self, + c:Component_T + ) -> Capsule: + + var tmp = Capsule() + tmp.type = T.component_name() + tmp.instance_name = c.instance_name + tmp.value=T.InitializeStates().value + self.states.append(tmp) + return tmp + + fn GetInstance[T:Component]( + inout self, + c:Component_T + )->Capsule: + var tmp_t = T.component_name() + for k in self.states: + if k[].type == tmp_t and k[].instance_name == c.instance_name: + return k[] + + return self.CreateInstance[T](c) + + fn TsRenderInto[T:Component]( + inout self, + inout e:Element, + c: Component_T + ): + var instance = self.GetInstance[T](c)#T() + + var tmp=T.Render(instance,c.props) + + tmp.attributes["data-instance_name"]=c.instance_name + e=tmp + + for k in self.states: + if k[].instance_name == c.instance_name: + k[].rendered=True + + fn delete_instances[only_unrendered:Bool=False](inout self): + var deleted = List[String]() + for s in self.states: + @parameter + if only_unrendered: + if s[].rendered == False: deleted.append(s[].instance_name) + else: + deleted.append(s[].instance_name) + + print("__del__() :",len(deleted)) + for i in deleted: + var x = 0 + for k in self.states: + if k[].instance_name == i[]: + @parameter + fn Iter[I:Int](): + if Ts[I].component_name()== k[].type: + Ts[I].DeinitializeStates(k[]) + unroll[Iter,len(VariadicList(Ts))]() + _=self.states.pop(x) + x+=1 + print("\t",i[]) + + + fn InstanceOf[InstanceName:String](inout self)-> Capsule: + for i in self.states: + if i[].instance_name == InstanceName: + return i[] + return Capsule() + + +fn is_component[T:Component](name:String)->Bool: + return T.component_name()==name + +fn TsEquals[T:Component,T2:Component]()->Bool: + return T.component_name() == T2.component_name() + +fn TsIndex[*Ts:Component](name:String)->Int: + var result=-1 + @parameter + fn loop[I:Int](): + if is_component[Ts[I]](name): result = I + unroll[loop,len(VariadicList(Ts))]() + return result + +fn TsCount[T:Component,*Ts:Component]()->Int: + var result=0 + @parameter + fn loop[I:Int](): + if TsEquals[Ts[I],T](): result += 1 + unroll[loop,len(VariadicList(Ts))]() + return result + +fn TsUniques[*Ts:Component]()->Bool: + var result = True + @parameter + fn loop[I:Int](): + if TsCount[Ts[I],Ts]()>1: + result = False + unroll[loop,len(VariadicList(Ts))]() + return result + +fn TsContains[*Ts:Component,name:StringLiteral]()->Bool: + var result = False + @parameter + fn loop[I:Int](): + result = result|TsEq[Ts[I],name]() + unroll[loop,len(VariadicList(Ts))]() + return result + +fn TsEq[Ts:Component,arg:StringLiteral]()->Bool: + return Ts.component_name() == arg + \ No newline at end of file diff --git a/struct composability prototype, with staticmethods/DomEvent.mojo b/struct composability prototype, with staticmethods/DomEvent.mojo new file mode 100644 index 0000000..fd4d668 --- /dev/null +++ b/struct composability prototype, with staticmethods/DomEvent.mojo @@ -0,0 +1,22 @@ +from utils.variant import Variant +from Component import * +from ComponentManager import * + +@value +struct DomEvent: + alias MouseEnter = "MouseEnter" + alias Click = "Click" + alias Change = "Change" + var target:String #TODO: id attribute + var type: String #TODO: "MouseEnter" + var name:String # On[DomEvent.Click]("name") + var data:String #TODO: json, depend on event type + +@value +struct EventHandler: + var type: StringLiteral + var instance_name:String + var event_name:String + +fn On[e:StringLiteral](name:StringLiteral)->EventHandler: + return EventHandler(e,"",name) diff --git a/struct composability prototype, with staticmethods/DomTree.mojo b/struct composability prototype, with staticmethods/DomTree.mojo new file mode 100644 index 0000000..4606b49 --- /dev/null +++ b/struct composability prototype, with staticmethods/DomTree.mojo @@ -0,0 +1,160 @@ +from utils.variant import Variant +from Helpers import * +from DomEvent import EventHandler +from collections import Optional +from Component import Component_T, ComponentStateless +from ComponentManager import * +from app import m + +#TODO: Splat: fn H[T:List[Variant[Component,Html]]]()->Element) + +fn H[T:Component]( + InstanceName:String, +)->Element: + var tmp = Element("") + + tmp.component = Component_T( + instance_name=InstanceName, + component_name=T.component_name(), + props = Capsule() + ) + m.RenderComponentIntoElements[False,T](tmp) + + return tmp + +fn H[T:Component,TProps:CollectionElement]( + InstanceName:String, + owned Props: TProps +)->Element: + var tmp = Element("") + var p = Capsule(Props^) + p.type="props" + tmp.component = Component_T( + instance_name=InstanceName, + component_name=T.component_name(), + props = p + ) + m.RenderComponentIntoElements[False,T](tmp) + + p.Del[TProps]() + return tmp^ + +fn H[T:ComponentStateless,TProps:CollectionElement]( + owned Props: TProps +)->Element: + var p = Capsule(Props^) + p.type="props" + var tmp = T.Render(p) + p.Del[TProps]() + return tmp^ + +fn H[T:ComponentStateless]( +)->Element: + var p = Capsule() + var tmp = T.Render(p^) + return tmp^ + +trait Html: + @staticmethod + fn tag()->String: ... + +fn H[T:Html]( + *args: + Variant[ + Element, + String, + StringLiteral, + CSS_T, + EventHandler + ], + **kwargs: String +) -> Element: + var tmp = Element(T.tag()) + tmp.component = None + for k in kwargs: + try:tmp.attributes[k[]]=kwargs[k[]] + except e:print(e) + if len(args)>0: + for i in range(len(args)): + var arg = args[i] + if arg.isa[Element](): + tmp.inner.append(arg.take[Element]()) + if arg.isa[String](): + var tmp3 = Element("TextNode") + tmp3.attributes["value"] = arg.take[String]() + tmp.inner.append(tmp3) + if arg.isa[StringLiteral](): + var tmp3 = Element("TextNode") + tmp3.attributes["value"] = arg.take[StringLiteral]() + tmp.inner.append(tmp3) + if arg.isa[EventHandler](): + var tmp_arg = arg.take[EventHandler]() + tmp.attributes[String("data-event_")+tmp_arg.type]= + tmp_arg.event_name + if arg.isa[CSS_T](): + tmp.attributes["style"]=arg.take[CSS_T]().value + return tmp +@value +struct Element: + var component: Optional[Component_T] + var tag: String + var inner:List[Variant[Self,NoneType]] + var attributes: Dict[String,String] + + fn __init__(inout self,tag:String): + self.component = None + self.tag=tag + self.inner=List[Variant[Self,NoneType]]() + self.attributes = Dict[String,String]() + + fn __copyinit__(inout self,other:Self): + self.component = other.component + self.tag= other.tag + self.inner = other.inner + self.attributes = other.attributes + fn __moveinit__(inout self,owned other:Self): + self.component = other.component + self.tag= other.tag + self.inner = other.inner + self.attributes = other.attributes + + +@value +struct Div: + @staticmethod + fn tag()->String: return "Div" +@value +struct H1: + @staticmethod + fn tag()->String: return "H1" +@value +struct H2: + @staticmethod + fn tag()->String: return "H1" +@value +struct A: + @staticmethod + fn tag()->String: return "a" +@value +struct Span: + @staticmethod + fn tag()->String: return "span" +@value +struct Button: + @staticmethod + fn tag()->String: return "button" + +@value +struct Br: + @staticmethod + fn tag()->String: return "br" + +@value +struct Input: + @staticmethod + fn tag()->String: return "input" + + + + + diff --git a/struct composability prototype, with staticmethods/Helpers.mojo b/struct composability prototype, with staticmethods/Helpers.mojo new file mode 100644 index 0000000..9d97c58 --- /dev/null +++ b/struct composability prototype, with staticmethods/Helpers.mojo @@ -0,0 +1,30 @@ +from utils.variant import Variant + +#f["hello {message} {emoji}"](message="world", emoji="🔥") +fn f[fmt:StringLiteral](**kwargs:String) ->String: + var res = String(fmt) + try: + for k in kwargs: + if(kwargs[k[]].count("{")+kwargs[k[]].count("}"))!=0: + raise Error("simple safeguard: remplacement value could contains {}") + try: res=res.replace("{"+k[]+"}",kwargs[k[]]) + except e: + res = "Error raised by f" #TODO: better + raise e + except e: print("f formatting function:",e) + return res + + +#print(CSS(color="red")) +@value +struct CSS_T: + var value:String + +fn CSS(**args:String)->CSS_T: + var res = CSS_T("") + + for a in args: + try: res.value+=a[]+":"+args[a[]]+";" + except e: print("CSS function, render == True :",e) + + return res diff --git a/struct composability prototype, with staticmethods/README.md b/struct composability prototype, with staticmethods/README.md new file mode 100644 index 0000000..fc6747e --- /dev/null +++ b/struct composability prototype, with staticmethods/README.md @@ -0,0 +1,283 @@ +# Integration prototype 🔥❤️🐍 + +> [!NOTE] +> This prototype was made to ameliorate design of LSX. +> It should not be used for anything in production (lots of UnsafePointer). + +Hope this will be useful to people that wan't to make web-frameworks too + +Lukas is working on lsx, the goal is to use 100% lsx with a wrapper on top if necessary + +Currently, it's just prototyping with the idea to see what to do + +# ⚠️ +- Start a socket, http-server: ```ip: 127.0.0.1 port: 8080``` +- Probably many unfreed pointers (need help) +- No current support for multiple clients/visitors, a session system would be neededdd + +# Why +- By having another use case for the design, we can identify what could be done in order to ameliorate it. +- Its fun to program in mojo +- further the development of web-frameworks + +# What is done so far: +- Reusable components +- Stateless components +- init and deinit on mount and unmount +- Rendering from a tree on the client-side with JS +- Basic event passing to instances of components +- Props + +# Challenges +- Instance name cannot use underscores (don't know why) +- Probably more things + +# Current conclusions +- LSX is a nice and expressive way to build a DOM, deserves attention and work + - great degree of composability + - very user-friendly (**kwargs and *args, for example) +- Mojo is awesome + +# Available features that could bring more to LSX: + - functions with same name and different signature ! + +# Possible feature request: + - ```__struct_name_of[App]() -> StringLiteral: "App"``` (struct name) + +# Example: +```mojo +from Helpers import * +from DomEvent import * +from DomTree import * +from Component import * +from ComponentManager import * +from Server import * +from css import * + +alias m_ = ComponentTypes[ + App, + Menu, + About, + Home, + CounterWidget +] + +var m=ComponentManager[ + m_.Ts +]() + +fn main(): + + var s = Server() + s.extra_css = spin_css + + s.Start[App]() + + for i in range(20): + s.ProcessRequest[App]() + + s^.Stop() + + +@value +struct AppState(): + var page:String + var title:String + +struct App(Component): + @staticmethod + fn component_name()->String: return "App" + + @staticmethod + fn InitializeStates()->Capsule: + return AppState("Home","Default title") + + @staticmethod + fn DeinitializeStates(owned states:Capsule): + states.Del[AppState]() + + @staticmethod + fn Event(owned self:Capsule , e:DomEvent): + if e.name == "title_edited": + self[AppState][].title=e.data + + @staticmethod + fn Render(owned states: Capsule, owned props:Capsule) -> Element: + var tmp = H[Div]() + if states[AppState][].page == "about": + tmp = H[About]("aboutinstance") + if states[AppState][].page == "home": + tmp = H[Home]("homeinstance") + + return + H[Div]( + CSS( + width="fit-content", + ), + H[H1](states[AppState][].title,CSS(`background-color`="deeppink")), + H[Input]( + On[DomEvent.Change]("title_edited"), + value=states[AppState][].title, + type="text" + ), + H[H1]( + "🐣", + CSS( + animation="spin 2s infinite", + width="fit-content", + `font-size`="128px", + margin="0" + ) + ), + H[Menu]("menuinstance"), + tmp, + H[CounterWidget]("CounterA",String("Increment")), + H[CounterWidget]("CounterB",String("Increment")), + H[Br](),H[Br](), + H[HelloWorld](), + H[HelloWorld](states[AppState][].title), + + ) + +struct Menu(Component): + @staticmethod + fn component_name()->String: return "Menu" + + @staticmethod + fn InitializeStates()->Capsule: return Capsule() + @staticmethod + fn DeinitializeStates(owned states:Capsule):... + + @staticmethod + fn Event(owned states:Capsule , e:DomEvent): + var i = m.InstanceOf["AppInstance"]() + if i: + if e.name == "aboutevent": + i[AppState][].page="about" + if e.name == "homeevent": + i[AppState][].page="home" + + + @staticmethod + fn Render(owned states: Capsule, owned props:Capsule) -> Element: + var tmp = CSS(height=32,margin=16) + return + H[Div]( + H[Button]( + "About", + On[DomEvent.Click]("aboutevent"), + tmp + ), + + H[Button]( + "Home", + On[DomEvent.Click]("homeevent"), + tmp + ) + ) + +struct About(Component): + @staticmethod + fn component_name()->String: return "About" + + + @staticmethod + fn InitializeStates()->Capsule: + return String("Hello world") + @staticmethod + fn DeinitializeStates(owned states:Capsule): + states.Del[String]() + + + @staticmethod + fn Event(owned states:Capsule , e:DomEvent): + ... + + @staticmethod + fn Render(owned states: Capsule, owned props:Capsule) -> Element: + return + H[Div]( + CSS( + `background-color`="floralwhite", + color="aquamarine" + ), + H[H1]( + "about page" + ), + H[Span](states[String][]) + ) + +struct Home(Component): + @staticmethod + fn component_name()->String: return "Home" + + + @staticmethod + fn InitializeStates()->Capsule: return Capsule() + @staticmethod + fn DeinitializeStates(owned states:Capsule):... + + + @staticmethod + fn Event(owned states:Capsule , e:DomEvent): + ... + + @staticmethod + fn Render(owned states: Capsule, owned props:Capsule) -> Element: + return + H[Div]( + CSS( + `background-color`="yellow", + color="blue" + ), + H[Span]( + "home page" + ) + ) + +struct CounterWidget(Component): + @staticmethod + fn component_name()->String: return "CounterWidget" + + + @staticmethod + fn InitializeStates()->Capsule: + return Capsule(Int(0)) + @staticmethod + fn DeinitializeStates(owned states:Capsule): + states.Del[Int]() + + + @staticmethod + fn Event(owned states:Capsule , e:DomEvent): + var p = m.InstanceOf["AppInstance"]() + if e.name == "change_app_title" and p: + states[Int][]+=1 + p[AppState][].title= + states.instance_name+": "+str(states[Int][]) + + @staticmethod + fn Render(owned states: Capsule, owned props:Capsule) -> Element: + var tmp = CSS(margin=8,`background-color`="skyblue") + var tmp2 = CSS(`font-size`=16+states[Int][]) + if props: + return H[Button]( + props[String][], + On[DomEvent.Click]("change_app_title"), + tmp,tmp2 + ) + return H[Button]("Default",tmp) + +struct HelloWorld(ComponentStateless): + @staticmethod + fn Render(owned Props:Capsule)->Element: + if Props: + return H[Div]( + H[Span]("props:"), + H[Span](Props[String][]) + ) + else: + return H[Div]( + H[Span]("Hello world") + ) +``` \ No newline at end of file diff --git a/struct composability prototype, with staticmethods/Server.mojo b/struct composability prototype, with staticmethods/Server.mojo new file mode 100644 index 0000000..4d89d9e --- /dev/null +++ b/struct composability prototype, with staticmethods/Server.mojo @@ -0,0 +1,137 @@ +from ComponentManager import * +from python import Python +from app import m,m_ + +@value +struct Server: + var socket:PythonObject + var js_renderer: String + var extra_css:String + var url:String + fn __init__(inout self): + self.socket=None + try: + with open("js_renderer.js","r") as f: + self.js_renderer = f.read() + except e: + print("Error importing js_renderer.js: " + str(e)) + self.js_renderer = "" + #TODO: set self.error = True + self.extra_css = " " + self.url = "" + + fn ProcessRequest[App_T:Component](inout self): + print("ok") + try: + var to_json = Python.import_module("json").loads + print("request loop-----------------") + var client = self.socket.accept() + if client[1][0] != '127.0.0.1': + print("Exit, request from: "+str(client[1][0])) + return + + var request = client[0].recv(1024).decode() + var full_request = request.split('\n') + request = request.split('\n')[0].split(" ") + if request[1]=="/": + var response:String = "HTTP/1.0 200 OK\n\n" + response += "
" + response += "" + response += "" + response += "" + response+="" + client[0].sendall(PythonObject(response).encode()) + client[0].close() + elif request[1]=="/styles.css": + var response:String = "HTTP/1.0 200 OK\n\n" + response += self.extra_css + client[0].sendall(PythonObject(response).encode()) + client[0].close() + elif request[1]=="/event": + var json_req = to_json( + full_request[-1] + ) + var response:String = "HTTP/1.0 200 OK\n\n" + + if str(json_req["type"]) == "DomEvent": + #print(json_req) + var instance_name = str(json_req["instance_name"]) + var event_name = str(json_req["event_name"]) + var event_data = str(json_req["data"]) + print(json_req) + for instance in m.states: + if instance[].instance_name==instance_name: + + @parameter + fn _loop[I:Int](): + if is_component[m_.Ts[I]](instance[].type): + var tmp_capsule:Capsule = instance[] + m_.Ts[I].Event(tmp_capsule , DomEvent( + "", + "type", + event_name, + event_data, + ) + ) + + unroll[_loop,len(VariadicList(m_.Ts))]() + + _=event_name + _=instance_name + _=event_data + + for s in m.states: + s[].rendered=False + + var elements = H[App_T]("AppInstance") + + response += m.RenderIntoJson[ + True + ](elements) + + m.delete_instances[ + only_unrendered=True + ]() + + print("self.states:") + for i in m.states: + print("\t",i[].instance_name) + #print("\t\t",i[].value) + + client[0].sendall(PythonObject(response).encode()) + client[0].close() + except e: print(e) + + fn Stop(owned self): + m.delete_instances() + try:self.socket.close() + except e:print(e) + Self.__del__(self^) + + fn Start[App_T:Component](inout self): + constrained[ + TsCount[App_T,m_.Ts](), + "fn Start[App_T:Component](inout self), App_T not in m_.Ts" + ]() + try: + var socket = Python.import_module("socket") + var r = Reference(self.socket) + r[] = socket.socket( + socket.AF_INET, + socket.SOCK_STREAM + ) + r[].setsockopt( + socket.SOL_SOCKET, + socket.SO_REUSEADDR, + 1 + ) + var host = "127.0.0.1" + var port = 8080 + self.socket.bind((host, port)) + self.socket.listen(1) + + self.url = "http://"+str(host)+":"+port + print(self.url) + + except e: + print(e) diff --git a/struct composability prototype, with staticmethods/app.mojo b/struct composability prototype, with staticmethods/app.mojo new file mode 100644 index 0000000..ef7d04f --- /dev/null +++ b/struct composability prototype, with staticmethods/app.mojo @@ -0,0 +1,234 @@ +from Helpers import * +from DomEvent import * +from DomTree import * +from Component import * +from ComponentManager import * +from Server import * +from css import * + +alias m_ = ComponentTypes[ + App, + Menu, + About, + Home, + CounterWidget +] + +var m=ComponentManager[ + m_.Ts +]() + +fn main(): + + var s = Server() + s.extra_css = spin_css + + s.Start[App]() + + for i in range(20): + s.ProcessRequest[App]() + + s^.Stop() + + +@value +struct AppState(): + var page:String + var title:String + +struct App(Component): + @staticmethod + fn component_name()->String: return "App" + + @staticmethod + fn InitializeStates()->Capsule: + return AppState("Home","Default title") + + @staticmethod + fn DeinitializeStates(owned states:Capsule): + states.Del[AppState]() + + @staticmethod + fn Event(owned self:Capsule , e:DomEvent): + if e.name == "title_edited": + self[AppState][].title=e.data + + @staticmethod + fn Render(owned states: Capsule, owned props:Capsule) -> Element: + var tmp = H[Div]() + if states[AppState][].page == "about": + tmp = H[About]("aboutinstance") + if states[AppState][].page == "home": + tmp = H[Home]("homeinstance") + + return + H[Div]( + CSS( + width="fit-content", + ), + H[H1](states[AppState][].title,CSS(`background-color`="deeppink")), + H[Input]( + On[DomEvent.Change]("title_edited"), + value=states[AppState][].title, + type="text" + ), + H[H1]( + "🐣", + CSS( + animation="spin 2s infinite", + width="fit-content", + `font-size`="128px", + margin="0" + ) + ), + H[Menu]("menuinstance"), + tmp, + H[CounterWidget]("CounterA",String("Increment")), + H[CounterWidget]("CounterB",String("Increment")), + H[Br](),H[Br](), + H[HelloWorld](), + H[HelloWorld](states[AppState][].title), + + ) + +struct Menu(Component): + @staticmethod + fn component_name()->String: return "Menu" + + @staticmethod + fn InitializeStates()->Capsule: return Capsule() + @staticmethod + fn DeinitializeStates(owned states:Capsule):... + + @staticmethod + fn Event(owned states:Capsule , e:DomEvent): + var i = m.InstanceOf["AppInstance"]() + if i: + if e.name == "aboutevent": + i[AppState][].page="about" + if e.name == "homeevent": + i[AppState][].page="home" + + + @staticmethod + fn Render(owned states: Capsule, owned props:Capsule) -> Element: + var tmp = CSS(height=32,margin=16) + return + H[Div]( + H[Button]( + "About", + On[DomEvent.Click]("aboutevent"), + tmp + ), + + H[Button]( + "Home", + On[DomEvent.Click]("homeevent"), + tmp + ) + ) + +struct About(Component): + @staticmethod + fn component_name()->String: return "About" + + + @staticmethod + fn InitializeStates()->Capsule: + return String("Hello world") + @staticmethod + fn DeinitializeStates(owned states:Capsule): + states.Del[String]() + + + @staticmethod + fn Event(owned states:Capsule , e:DomEvent): + ... + + @staticmethod + fn Render(owned states: Capsule, owned props:Capsule) -> Element: + return + H[Div]( + CSS( + `background-color`="floralwhite", + color="aquamarine" + ), + H[H1]( + "about page" + ), + H[Span](states[String][]) + ) + +struct Home(Component): + @staticmethod + fn component_name()->String: return "Home" + + + @staticmethod + fn InitializeStates()->Capsule: return Capsule() + @staticmethod + fn DeinitializeStates(owned states:Capsule):... + + + @staticmethod + fn Event(owned states:Capsule , e:DomEvent): + ... + + @staticmethod + fn Render(owned states: Capsule, owned props:Capsule) -> Element: + return + H[Div]( + CSS( + `background-color`="yellow", + color="blue" + ), + H[Span]( + "home page" + ) + ) + +struct CounterWidget(Component): + @staticmethod + fn component_name()->String: return "CounterWidget" + + + @staticmethod + fn InitializeStates()->Capsule: + return Capsule(Int(0)) + @staticmethod + fn DeinitializeStates(owned states:Capsule): + states.Del[Int]() + + + @staticmethod + fn Event(owned states:Capsule , e:DomEvent): + var p = m.InstanceOf["AppInstance"]() + if e.name == "change_app_title" and p: + states[Int][]+=1 + p[AppState][].title= + states.instance_name+": "+str(states[Int][]) + + @staticmethod + fn Render(owned states: Capsule, owned props:Capsule) -> Element: + var tmp = CSS(margin=8,`background-color`="skyblue") + var tmp2 = CSS(`font-size`=16+states[Int][]) + if props: + return H[Button]( + props[String][], + On[DomEvent.Click]("change_app_title"), + tmp,tmp2 + ) + return H[Button]("Default",tmp) + +struct HelloWorld(ComponentStateless): + @staticmethod + fn Render(owned Props:Capsule)->Element: + if Props: + return H[Div]( + H[Span]("props:"), + H[Span](Props[String][]) + ) + else: + return H[Div]( + H[Span]("Hello world") + ) \ No newline at end of file diff --git a/struct composability prototype, with staticmethods/css.mojo b/struct composability prototype, with staticmethods/css.mojo new file mode 100644 index 0000000..aa30451 --- /dev/null +++ b/struct composability prototype, with staticmethods/css.mojo @@ -0,0 +1,16 @@ +alias spin_css=""" + body { + text-align: -moz-center; + font-family: monospace; + backgroung-color: teal; + } + + @keyframes spin { + from { + transform:rotate(0deg); + } + to { + transform:rotate(360deg); + } + } + """ \ No newline at end of file diff --git a/struct composability prototype, with staticmethods/js_renderer.js b/struct composability prototype, with staticmethods/js_renderer.js new file mode 100644 index 0000000..9eb13ee --- /dev/null +++ b/struct composability prototype, with staticmethods/js_renderer.js @@ -0,0 +1,78 @@ +async function Event(data) { + const response = await fetch("/event", { + method: "POST", + headers: { "Content-Type": "application/json",}, + body: JSON.stringify(data), + }); + + const json = await response.json(); + var render_res = Render(json,json["instance_name"]) + var b = document.createElement("BODY") + b.appendChild(render_res) + document.body=b + +} +window.addEventListener("load", (event) => { + const data = { type: "first render" }; + Event(data); +}); + +function Render(json,instance_name){ + var instance_n = instance_name + if (json.hasOwnProperty("data-instance_name")){ + instance_n = json["data-instance_name"] + } + var tag = json["tag"] + if (tag == "TextNode"){ + return document.createTextNode(json["value"]); + } + var inner = json["inner"] + delete json["inner"]; + var el = document.createElement(tag); + for (attr in json){ + if (attr.startsWith("data-event_")){ + console.log(attr,json[attr]) + var ev = attr.split("data-event_")[1] + var ev_val = json[attr] + if (ev == "Click"){ + el.addEventListener("click",(e)=>{ + Event({ + type: "DomEvent" , + event_name: ev_val, + instance_name: instance_n, + data:"" + }); + }) + } + if (ev == "MouseEnter"){ + el.addEventListener("mouseenter",(e)=>{ + console.log(ev_val) + Event({ + type: "DomEvent" , + event_name: ev_val, + instance_name: instance_n, + data:"" + }); + }) + } + if (ev == "Change"){ + el.addEventListener("change",(e)=>{ + console.log(ev_val,e.target.value) + Event({ + type: "DomEvent" , + event_name: ev_val, + instance_name: instance_n, + data: e.target.value + }); + }) + } + } + el.setAttribute(attr,json[attr]) + } + inner.forEach((inner_el) => { + el.appendChild(Render(inner_el,instance_n)) + }); + return el + +} + diff --git a/struct composability prototype, with staticmethods/todo.md b/struct composability prototype, with staticmethods/todo.md new file mode 100644 index 0000000..0d439ac --- /dev/null +++ b/struct composability prototype, with staticmethods/todo.md @@ -0,0 +1 @@ +- \ No newline at end of file diff --git a/struct composability prototype, with variant/AnyThing.mojo b/struct composability prototype, with variant/AnyThing.mojo new file mode 100644 index 0000000..c85a2b5 --- /dev/null +++ b/struct composability prototype, with variant/AnyThing.mojo @@ -0,0 +1,9 @@ +from utils.variant import Variant +alias SmallVariant = Variant[ + Int64, + Float64, + Bool, + String, + StringLiteral +] +alias ListSmallVariant = List[SmallVariant] \ No newline at end of file diff --git a/struct composability prototype, with variant/Component.mojo b/struct composability prototype, with variant/Component.mojo new file mode 100644 index 0000000..5907d16 --- /dev/null +++ b/struct composability prototype, with variant/Component.mojo @@ -0,0 +1,19 @@ +from DomTree import * +from DomEvent import * + +trait Component: + @staticmethod + fn component_name()->String:... + @staticmethod + fn InitialStates()->ListSmallVariant: ... + @staticmethod + fn Event(owned self_instance:WrapperObject, e:DomEvent)->List[WrapperObject]: ... + @staticmethod + fn Render(owned states:ListSmallVariant,owned props:ListSmallVariant) ->Element: ... + +@value +struct Component_T: + var instance_name:String + var component_name: String + var props: ListSmallVariant + diff --git a/struct composability prototype, with variant/ComponentManager.mojo b/struct composability prototype, with variant/ComponentManager.mojo new file mode 100644 index 0000000..1e06ab8 --- /dev/null +++ b/struct composability prototype, with variant/ComponentManager.mojo @@ -0,0 +1,178 @@ +from Component import * +from DomTree import * +from python import Python +from AnyThing import * + +def PythonDict()->PythonObject: + var tmp_result = Python.evaluate("lambda **kwargs: kwargs") + return tmp_result(empty=True) + +@value +struct WrapperObject(CollectionElement): + var value: ListSmallVariant + var type: String + var instance_name: String + var rendered: Bool + fn __init__(inout self): + self.value = ListSmallVariant() + self.rendered = False + self.type= "default" + self.instance_name = "default" +@value +struct ComponentManager[*Ts:Component]: + var states: List[WrapperObject] + + fn __init__(inout self): + constrained[ + TsUniques[Ts](), + "Two components have the same name" + ]() + + self.states = __type_of(self.states)() + + fn RenderIntoJson[First:Bool=False]( + inout self, + inout elements: Element, + )->PythonObject: + try: + var tmp_result = Python.evaluate("lambda **kwargs: kwargs")( + tag=elements.tag, + inner=[] + ) + if elements.tag == "TextNode": + tmp_result["value"]=elements.attributes["value"] + else: + for a in elements.attributes: + tmp_result[a[]]=elements.attributes[a[]] + for i in elements.inner: + if i[].isa[Element](): + tmp_result["inner"].append( + self.RenderIntoJson( + i[]._get_ptr[Element]()[] + ) + ) + @parameter + if First: return Python.import_module("json").dumps(tmp_result) + else: return tmp_result + except e: print("RenderToJson",e) + return None + + fn RenderComponentsIntoElements[first:Bool]( + inout self, + inout arg: Element + ): + #TODO: check not instance_name rendered twice + + + if arg.component: + var tmp = arg.component.take() + @parameter + fn loop[I:Int](): + if is_component[Ts[I]](tmp.component_name): + self.TsRenderInto[Ts[I]](arg,tmp) + unroll[loop,len(VariadicList(Ts))]() + _=tmp + + for i in arg.inner: + if i[].isa[Element](): + if i[].get[Element]()[].component: + var tmp = i[].get[Element]()[] + self.RenderComponentsIntoElements[False](tmp) + i[] = tmp + + fn CreateInstance[T:Component]( + inout self, + c:Component_T + ) -> WrapperObject: + + var tmp = WrapperObject() + tmp.type = T.component_name() + tmp.instance_name = c.instance_name + tmp.value=T.InitialStates() + self.states.append(tmp) + return tmp + + fn GetInstance[T:Component]( + inout self, + c:Component_T + )->WrapperObject: + var tmp_t = T.component_name() + for k in self.states: + if k[].type == tmp_t and k[].instance_name == c.instance_name: + return k[] + + return self.CreateInstance[T](c) + + fn TsRenderInto[T:Component]( + inout self, + inout e:Element, + c: Component_T + ): + var instance = self.GetInstance[T](c)#T() + + var tmp=T.Render(instance.value,c.props) + tmp.attributes["data-instance_name"]=c.instance_name + e=tmp + + for k in self.states: + if k[].instance_name == c.instance_name: + k[].rendered=True + + fn delete_instances[only_unrendered:Bool=False](inout self): + var deleted = List[String]() + for s in self.states: + if s[].rendered == False: deleted.append(s[].instance_name) + + + print("__del__() :",len(deleted)) + for i in deleted: + var x = 0 + for k in self.states: + if k[].instance_name == i[]: + _=self.states.pop(x) + x+=1 + print("\t",i[]) + + +fn is_component[T:Component](name:String)->Bool: + return T.component_name()==name + +fn TsEquals[T:Component,T2:Component]()->Bool: + return T.component_name() == T2.component_name() + +fn TsIndex[*Ts:Component](name:String)->Int: + var result=-1 + @parameter + fn loop[I:Int](): + if is_component[Ts[I]](name): result = I + unroll[loop,len(VariadicList(Ts))]() + return result + +fn TsCount[T:Component,*Ts:Component]()->Int: + var result=0 + @parameter + fn loop[I:Int](): + if TsEquals[Ts[I],T](): result += 1 + unroll[loop,len(VariadicList(Ts))]() + return result + +fn TsUniques[*Ts:Component]()->Bool: + var result = True + @parameter + fn loop[I:Int](): + if TsCount[Ts[I],Ts]()>1: + result = False + unroll[loop,len(VariadicList(Ts))]() + return result + +fn TsContains[*Ts:Component,name:StringLiteral]()->Bool: + var result = False + @parameter + fn loop[I:Int](): + result = result|TsEq[Ts[I],name]() + unroll[loop,len(VariadicList(Ts))]() + return result + +fn TsEq[Ts:Component,arg:StringLiteral]()->Bool: + return Ts.component_name() == arg + \ No newline at end of file diff --git a/struct composability prototype, with variant/DomEvent.mojo b/struct composability prototype, with variant/DomEvent.mojo new file mode 100644 index 0000000..5025958 --- /dev/null +++ b/struct composability prototype, with variant/DomEvent.mojo @@ -0,0 +1,27 @@ +from utils.variant import Variant +from Component import * +from ComponentManager import * + +@value +struct InstanceWrapper: + var instances: List[WrapperObject] + var update_function: fn(inout instances: List[WrapperObject])->None + +@value +struct DomEvent: + alias MouseEnter = "MouseEnter" + alias Click = "Click" + var target:String #TODO: id attribute + var type: String #TODO: "MouseEnter" + var name:String # On[DomEvent.Click]("name") + var data:String #TODO: json, depend on event type + var Instances: List[WrapperObject] + +@value +struct EventHandler: + var type: StringLiteral + var instance_name:String + var event_name:String + +fn On[e:StringLiteral](name:StringLiteral)->EventHandler: + return EventHandler(e,"",name) diff --git a/struct composability prototype, with variant/DomTree.mojo b/struct composability prototype, with variant/DomTree.mojo new file mode 100644 index 0000000..5f469b2 --- /dev/null +++ b/struct composability prototype, with variant/DomTree.mojo @@ -0,0 +1,124 @@ +from utils.variant import Variant +from Helpers import * +from DomEvent import EventHandler +from collections import Optional +from Component import Component_T +from ComponentManager import * + +#TODO: Splat: fn H[T:List[Variant[Component,Html]]]()->Element) + +fn H[T:Component]( + InstanceName:String, + *args: SmallVariant +)->Element: + var tmp = Element("") + var tmp2 = ListSmallVariant() + for i in args: + tmp2.append(i[]) + tmp.component = Component_T( + instance_name=InstanceName, + component_name=T.component_name(), + props = tmp2 + ) + return tmp + + +trait Html: + @staticmethod + fn tag()->String: ... + +fn H[T:Html]( + *args: + Variant[ + Element, + String, + StringLiteral, + CSS_T, + EventHandler + ], + **kwargs: String +) -> Element: + var tmp = Element(T.tag()) + tmp.component = None + for k in kwargs: + try:tmp.attributes[k[]]=kwargs[k[]] + except e:print(e) + if len(args)>0: + for i in range(len(args)): + var arg = args[i] + if arg.isa[Element](): + tmp.inner.append(arg.take[Element]()) + if arg.isa[String](): + var tmp3 = Element("TextNode") + tmp3.attributes["value"] = arg.take[String]() + tmp.inner.append(tmp3) + if arg.isa[StringLiteral](): + var tmp3 = Element("TextNode") + tmp3.attributes["value"] = arg.take[StringLiteral]() + tmp.inner.append(tmp3) + if arg.isa[EventHandler](): + var tmp_arg = arg.take[EventHandler]() + tmp.attributes[String("data-event_")+tmp_arg.type]= + tmp_arg.event_name + if arg.isa[CSS_T](): + tmp.attributes["style"]=arg.take[CSS_T]().value + return tmp +@value +struct Element: + var component: Optional[Component_T] + var tag: String + var inner:List[Variant[Self,NoneType]] + var attributes: Dict[String,String] + + fn __init__(inout self,tag:String): + self.component = None + self.tag=tag + self.inner=List[Variant[Self,NoneType]]() + self.attributes = Dict[String,String]() + + fn __copyinit__(inout self,other:Self): + self.component = other.component + self.tag= other.tag + self.inner = other.inner + self.attributes = other.attributes + fn __moveinit__(inout self,owned other:Self): + self.component = other.component + self.tag= other.tag + self.inner = other.inner + self.attributes = other.attributes + + +@value +struct Div: + @staticmethod + fn tag()->String: return "Div" +@value +struct H1: + @staticmethod + fn tag()->String: return "H1" +@value +struct H2: + @staticmethod + fn tag()->String: return "H1" +@value +struct A: + @staticmethod + fn tag()->String: return "a" +@value +struct Span: + @staticmethod + fn tag()->String: return "span" +@value +struct Button: + @staticmethod + fn tag()->String: return "button" + +@value +struct Br: + @staticmethod + fn tag()->String: return "br" + + + + + diff --git a/struct composability prototype, with variant/Helpers.mojo b/struct composability prototype, with variant/Helpers.mojo new file mode 100644 index 0000000..9d97c58 --- /dev/null +++ b/struct composability prototype, with variant/Helpers.mojo @@ -0,0 +1,30 @@ +from utils.variant import Variant + +#f["hello {message} {emoji}"](message="world", emoji="🔥") +fn f[fmt:StringLiteral](**kwargs:String) ->String: + var res = String(fmt) + try: + for k in kwargs: + if(kwargs[k[]].count("{")+kwargs[k[]].count("}"))!=0: + raise Error("simple safeguard: remplacement value could contains {}") + try: res=res.replace("{"+k[]+"}",kwargs[k[]]) + except e: + res = "Error raised by f" #TODO: better + raise e + except e: print("f formatting function:",e) + return res + + +#print(CSS(color="red")) +@value +struct CSS_T: + var value:String + +fn CSS(**args:String)->CSS_T: + var res = CSS_T("") + + for a in args: + try: res.value+=a[]+":"+args[a[]]+";" + except e: print("CSS function, render == True :",e) + + return res diff --git a/struct composability prototype, with variant/README.md b/struct composability prototype, with variant/README.md new file mode 100644 index 0000000..93bc9fe --- /dev/null +++ b/struct composability prototype, with variant/README.md @@ -0,0 +1,3 @@ +Interesting design with variants! + +(untested, could have bugs so don't use) \ No newline at end of file diff --git a/struct composability prototype, with variant/Server.mojo b/struct composability prototype, with variant/Server.mojo new file mode 100644 index 0000000..d2e8eac --- /dev/null +++ b/struct composability prototype, with variant/Server.mojo @@ -0,0 +1,137 @@ +from ComponentManager import * +from python import Python + +@value +struct Server[*Ts:Component]: + var socket:PythonObject + var component_manager:ComponentManager[Ts] + var js_renderer: String + var extra_css:String + var url:String + fn __init__(inout self,owned arg:ComponentManager[Ts]): + self.socket=None + self.component_manager=arg + try: + with open("js_renderer.js","r") as f: + self.js_renderer = f.read() + except e: + print("Error importing js_renderer.js: " + str(e)) + self.js_renderer = "" + #TODO: set self.error = True + self.extra_css = " " + self.url = "" + + fn ProcessRequest[App_T:Component](inout self): + print("ok") + try: + var to_json = Python.import_module("json").loads + print("request loop-----------------") + var client = self.socket.accept() + if client[1][0] != '127.0.0.1': + print("Exit, request from: "+str(client[1][0])) + return + + var request = client[0].recv(1024).decode() + var full_request = request.split('\n') + request = request.split('\n')[0].split(" ") + if request[1]=="/": + var response:String = "HTTP/1.0 200 OK\n\n" + response += "" + response += "" + response += "" + response += "" + response+="" + client[0].sendall(PythonObject(response).encode()) + client[0].close() + elif request[1]=="/styles.css": + var response:String = "HTTP/1.0 200 OK\n\n" + response += self.extra_css + client[0].sendall(PythonObject(response).encode()) + client[0].close() + elif request[1]=="/event": + var json_req = to_json( + full_request[-1] + ) + var response:String = "HTTP/1.0 200 OK\n\n" + + if str(json_req["type"]) == "DomEvent": + #print(json_req) + var instance_name = str(json_req["instance_name"]) + var event_name = str(json_req["event_name"]) + + var cp = self.component_manager.states + print(json_req) + for instance in self.component_manager.states: + if instance[].instance_name==instance_name: + try: + var component_name = instance[].type + @parameter + fn _loop[I:Int](): + if is_component[Self.Ts[I]](component_name): + try: + cp = Ts[I].Event(instance[], DomEvent( + "", + "type", + event_name, + "data", + cp + ) + ) + except e: print('error component.event()',e) + unroll[_loop,len(VariadicList(Self.Ts))]() + except e: print("error ",e) + self.component_manager.states = cp + _=event_name + _=instance_name + + + var elements = H[App_T]("AppInstance") + for s in self.component_manager.states: + s[].rendered=False + self.component_manager.RenderComponentsIntoElements[ + first=True + ](elements) + + response += self.component_manager.RenderIntoJson[ + True + ](elements) + + self.component_manager.delete_instances[ + only_unrendered=True + ]() + + print("self.states:") + for i in self.component_manager.states: + print("\t",i[].instance_name) + #print("\t\t",i[].value) + + client[0].sendall(PythonObject(response).encode()) + client[0].close() + except e: print(e) + fn Start[App_T:Component](inout self): + constrained[ + TsCount[App_T,Ts]()==1, + "Component not registered in ComponentManager" + ]() + try: + var socket = Python.import_module("socket") + var r = Reference(self.socket) + r[] = socket.socket( + socket.AF_INET, + socket.SOCK_STREAM + ) + r[].setsockopt( + socket.SOL_SOCKET, + socket.SO_REUSEADDR, + 1 + ) + var host = "127.0.0.1" + var port = 8080 + self.socket.bind((host, port)) + self.socket.listen(1) + + self.url = "http://"+str(host)+":"+port + print(self.url) + + except e: + print(e) diff --git a/struct composability prototype, with variant/js_renderer.js b/struct composability prototype, with variant/js_renderer.js new file mode 100644 index 0000000..878cf91 --- /dev/null +++ b/struct composability prototype, with variant/js_renderer.js @@ -0,0 +1,65 @@ +async function Event(data) { + const response = await fetch("/event", { + method: "POST", + headers: { "Content-Type": "application/json",}, + body: JSON.stringify(data), + }); + + const json = await response.json(); + var render_res = Render(json,json["instance_name"]) + var b = document.createElement("BODY") + b.appendChild(render_res) + document.body=b + +} +window.addEventListener("load", (event) => { + const data = { type: "first render" }; + Event(data); +}); + +function Render(json,instance_name){ + var instance_n = instance_name + if (json.hasOwnProperty("data-instance_name")){ + instance_n = json["data-instance_name"] + } + var tag = json["tag"] + if (tag == "TextNode"){ + return document.createTextNode(json["value"]); + } + var inner = json["inner"] + delete json["inner"]; + var el = document.createElement(tag); + for (attr in json){ + if (attr.startsWith("data-event_")){ + console.log(attr,json[attr]) + var ev = attr.split("data-event_")[1] + var ev_val = json[attr] + if (ev == "Click"){ + el.addEventListener("click",(e)=>{ + Event({ + type: "DomEvent" , + event_name: ev_val, + instance_name: instance_n + }); + }) + } + if (ev == "MouseEnter"){ + el.addEventListener("mouseenter",(e)=>{ + console.log(ev_val) + Event({ + type: "DomEvent" , + event_name: ev_val, + instance_name: instance_n + }); + }) + } + } + el.setAttribute(attr,json[attr]) + } + inner.forEach((inner_el) => { + el.appendChild(Render(inner_el,instance_n)) + }); + return el + +} + diff --git a/struct composability prototype, with variant/main.mojo b/struct composability prototype, with variant/main.mojo new file mode 100644 index 0000000..921c97e --- /dev/null +++ b/struct composability prototype, with variant/main.mojo @@ -0,0 +1,166 @@ +from Helpers import * +from DomEvent import * +from DomTree import * +from Component import * +from ComponentManager import * +from Server import * + +fn main(): + var spin_css=""" + body { + text-align: -moz-center; + font-family: monospace; + backgroung-color: teal; + } + + @keyframes spin { + from { + transform:rotate(0deg); + } + to { + transform:rotate(360deg); + } + } + """ + var m = ComponentManager[ + App, + Menu, + About, + Home + ]() + var s = Server(m^) + s.extra_css = spin_css + s.Start[App]() + while True: + s.ProcessRequest[App]() + +struct App(Component): + @staticmethod + fn component_name()->String: return "App" + + @staticmethod + fn InitialStates()->ListSmallVariant: + var states = ListSmallVariant("Index","Default app title") + return states + + @staticmethod + fn Event(owned self_instance:WrapperObject , e:DomEvent)->List[WrapperObject]: + return e.Instances + + @staticmethod + fn Render(owned states: ListSmallVariant, owned props:ListSmallVariant) -> Element: + var tmp = H[Div]() + if states[0].take[StringLiteral]() == "about": + tmp = H[About]("aboutinstance") + if states[0].take[StringLiteral]() == "home": + tmp = H[Home]("homeinstance") + + return + H[Div]( + CSS( + width="fit-content", + ), + H[H1](states[1].take[StringLiteral](),CSS(`background-color`="deeppink")), + H[H1]( + "🐣", + CSS( + animation="spin 2s infinite", + width="fit-content", + `font-size`="128px", + margin="0" + ) + ), + H[Menu]("menuinstance"), + tmp + ) + +struct Menu(Component): + @staticmethod + fn component_name()->String: return "Menu" + + @staticmethod + fn InitialStates()->ListSmallVariant: return ListSmallVariant() + + @staticmethod + fn Event(owned self_instance:WrapperObject , e:DomEvent)->List[WrapperObject]: + var tmp = e.Instances + if e.name == "aboutevent": + + for i in tmp: + if i[].instance_name == "AppInstance": + i[].value[0]="about" + i[].value[1]= "Title: about" + + + if e.name == "homeevent": + for i in tmp: + if i[].instance_name == "AppInstance": + i[].value[0]= "home" + i[].value[1]= "Title: Home" + + return tmp + @staticmethod + fn Render(owned states: ListSmallVariant, owned props:ListSmallVariant) -> Element: + return + H[Div]( + H[Button]( + "About", + On[DomEvent.Click]("aboutevent") + ), + + H[Button]( + "Home", + On[DomEvent.Click]("homeevent") + ) + ) + + +struct About(Component): + @staticmethod + fn component_name()->String: return "About" + + @staticmethod + fn InitialStates()->ListSmallVariant: return ListSmallVariant() + + @staticmethod + fn Event(owned self_instance:WrapperObject , e:DomEvent)->List[WrapperObject]: + return e.Instances + + @staticmethod + fn Render(owned states: ListSmallVariant, owned props:ListSmallVariant) -> Element: + return + H[Div]( + CSS( + `background-color`="blue", + color="yellow" + ), + H[Span]( + "about page" + ) + ) + +struct Home(Component): + @staticmethod + fn component_name()->String: return "Home" + + @staticmethod + fn InitialStates()->ListSmallVariant: return ListSmallVariant() + + @staticmethod + fn Event(owned self_instance:WrapperObject , e:DomEvent)->List[WrapperObject]: + return e.Instances + + @staticmethod + fn Render(owned states: ListSmallVariant, owned props:ListSmallVariant) -> Element: + return + H[Div]( + CSS( + `background-color`="yellow", + color="blue" + ), + H[Span]( + "home page" + ) + ) + + diff --git a/struct composability prototype/Component.mojo b/struct composability prototype/Component.mojo new file mode 100644 index 0000000..b96b43b --- /dev/null +++ b/struct composability prototype/Component.mojo @@ -0,0 +1,25 @@ +from DomTree import * +from DomEvent import * + +struct ComponentTypes[*Ts:Component]:... + +trait Component(CollectionElement): + @staticmethod + fn component_name()->String:... + + fn __init__(inout self): ... + + fn Event(inout self, e:DomEvent): ... + + fn Render(inout self,owned props:Capsule) ->Element: ... + +trait ComponentStateless: + @staticmethod + fn Render(owned Props: Capsule)->Element:... + +@value +struct Component_T: + var instance_name:String + var component_name: String + var props: Capsule + diff --git a/struct composability prototype/ComponentManager.mojo b/struct composability prototype/ComponentManager.mojo new file mode 100644 index 0000000..77fb42b --- /dev/null +++ b/struct composability prototype/ComponentManager.mojo @@ -0,0 +1,205 @@ +from Component import * +from DomTree import * +from python import Python +from app import m_ + +def PythonDict()->PythonObject: + var tmp_result = Python.evaluate("lambda **kwargs: kwargs") + return tmp_result(empty=True) + +@value +struct Capsule(CollectionElement): + var value: UnsafePointer[NoneType] + var type: String + var instance_name: String + var rendered: Bool + fn __init__(inout self): + self.value = UnsafePointer[NoneType]() + self.rendered = False + self.type= "default" + self.instance_name = "default" + + fn __bool__(self)->Bool: return self.type!="default" + + fn __init__[T:CollectionElement](inout self,owned arg:T): + var tmp = UnsafePointer[T].alloc(1) + initialize_pointee_move(tmp,arg^) + self.value = tmp.bitcast[NoneType]() + self.rendered = False + self.type= "default" + self.instance_name = "default" + + + + fn __getitem__[T:AnyType](inout self)->Reference[ + T, + __mlir_attr.`1: i1`, + __lifetime_of(self) + ]: + return self.value.bitcast[T]().__refitem__() + + fn Del[T:CollectionElement](inout self): + destroy_pointee(self.value.bitcast[T]()) + self.value.free() + self.type = "default" + +@value +struct ComponentManager[*Ts:Component]: + var states: List[Capsule] + + fn __init__(inout self): + constrained[ + TsUniques[Ts](), + "Two components have the same name" + ]() + + self.states = __type_of(self.states)() + + fn RenderIntoJson[First:Bool=False]( + inout self, + inout elements: Element, + )->PythonObject: + try: + var tmp_result = Python.evaluate("lambda **kwargs: kwargs")( + tag=elements.tag, + inner=[] + ) + if elements.tag == "TextNode": + tmp_result["value"]=elements.attributes["value"] + else: + for a in elements.attributes: + tmp_result[a[]]=elements.attributes[a[]] + for i in elements.inner: + if i[].isa[Element](): + tmp_result["inner"].append( + self.RenderIntoJson( + i[]._get_ptr[Element]()[] + ) + ) + @parameter + if First: return Python.import_module("json").dumps(tmp_result) + else: return tmp_result + except e: print("RenderToJson",e) + return None + + fn RenderComponentIntoElements[first:Bool,T:Component]( + inout self, + inout arg: Element + ): + var tmp = arg.component.value()[] + self.TsRenderInto[T](arg,tmp) + + + fn CreateInstance[T:Component]( + inout self, + c:Component_T + ) -> Capsule: + + var tmp = Capsule(T()) + tmp.type = T.component_name() + tmp.instance_name = c.instance_name + self.states.append(tmp) + return tmp + + fn GetInstance[T:Component]( + inout self, + c:Component_T + )->Capsule: + var tmp_t = T.component_name() + for k in self.states: + if k[].type == tmp_t and k[].instance_name == c.instance_name: + return k[] + + return self.CreateInstance[T](c) + + fn TsRenderInto[T:Component]( + inout self, + inout e:Element, + c: Component_T + ): + var instance = self.GetInstance[T](c)#T() + var tmp_i = move_from_pointee(instance.value.bitcast[T]()) + var tmp=T.Render(tmp_i,c.props) + initialize_pointee_move(instance.value.bitcast[T](),tmp_i^) + tmp.attributes["data-instance_name"]=c.instance_name + e=tmp + + for k in self.states: + if k[].instance_name == c.instance_name: + k[].rendered=True + + fn delete_instances[only_unrendered:Bool=False](inout self): + var deleted = List[String]() + for s in self.states: + @parameter + if only_unrendered: + if s[].rendered == False: deleted.append(s[].instance_name) + else: + deleted.append(s[].instance_name) + + print("__del__() :",len(deleted)) + for i in deleted: + var x = 0 + for k in self.states: + if k[].instance_name == i[]: + @parameter + fn Iter[I:Int](): + if Ts[I].component_name()== k[].type: + var tmp_ = k[].value.bitcast[Ts[I]]() + _=move_from_pointee(tmp_) + tmp_.free() + unroll[Iter,len(VariadicList(Ts))]() + _=self.states.pop(x) + x+=1 + print("\t",i[]) + + + fn InstanceOf[InstanceName:String](inout self)-> Capsule: + for i in self.states: + if i[].instance_name == InstanceName: + return i[] + return Capsule() + + +fn is_component[T:Component](name:String)->Bool: + return T.component_name()==name + +fn TsEquals[T:Component,T2:Component]()->Bool: + return T.component_name() == T2.component_name() + +fn TsIndex[*Ts:Component](name:String)->Int: + var result=-1 + @parameter + fn loop[I:Int](): + if is_component[Ts[I]](name): result = I + unroll[loop,len(VariadicList(Ts))]() + return result + +fn TsCount[T:Component,*Ts:Component]()->Int: + var result=0 + @parameter + fn loop[I:Int](): + if TsEquals[Ts[I],T](): result += 1 + unroll[loop,len(VariadicList(Ts))]() + return result + +fn TsUniques[*Ts:Component]()->Bool: + var result = True + @parameter + fn loop[I:Int](): + if TsCount[Ts[I],Ts]()>1: + result = False + unroll[loop,len(VariadicList(Ts))]() + return result + +fn TsContains[*Ts:Component,name:StringLiteral]()->Bool: + var result = False + @parameter + fn loop[I:Int](): + result = result|TsEq[Ts[I],name]() + unroll[loop,len(VariadicList(Ts))]() + return result + +fn TsEq[Ts:Component,arg:StringLiteral]()->Bool: + return Ts.component_name() == arg + \ No newline at end of file diff --git a/struct composability prototype/DomEvent.mojo b/struct composability prototype/DomEvent.mojo new file mode 100644 index 0000000..fd4d668 --- /dev/null +++ b/struct composability prototype/DomEvent.mojo @@ -0,0 +1,22 @@ +from utils.variant import Variant +from Component import * +from ComponentManager import * + +@value +struct DomEvent: + alias MouseEnter = "MouseEnter" + alias Click = "Click" + alias Change = "Change" + var target:String #TODO: id attribute + var type: String #TODO: "MouseEnter" + var name:String # On[DomEvent.Click]("name") + var data:String #TODO: json, depend on event type + +@value +struct EventHandler: + var type: StringLiteral + var instance_name:String + var event_name:String + +fn On[e:StringLiteral](name:StringLiteral)->EventHandler: + return EventHandler(e,"",name) diff --git a/struct composability prototype/DomTree.mojo b/struct composability prototype/DomTree.mojo new file mode 100644 index 0000000..4606b49 --- /dev/null +++ b/struct composability prototype/DomTree.mojo @@ -0,0 +1,160 @@ +from utils.variant import Variant +from Helpers import * +from DomEvent import EventHandler +from collections import Optional +from Component import Component_T, ComponentStateless +from ComponentManager import * +from app import m + +#TODO: Splat: fn H[T:List[Variant[Component,Html]]]()->Element) + +fn H[T:Component]( + InstanceName:String, +)->Element: + var tmp = Element("") + + tmp.component = Component_T( + instance_name=InstanceName, + component_name=T.component_name(), + props = Capsule() + ) + m.RenderComponentIntoElements[False,T](tmp) + + return tmp + +fn H[T:Component,TProps:CollectionElement]( + InstanceName:String, + owned Props: TProps +)->Element: + var tmp = Element("") + var p = Capsule(Props^) + p.type="props" + tmp.component = Component_T( + instance_name=InstanceName, + component_name=T.component_name(), + props = p + ) + m.RenderComponentIntoElements[False,T](tmp) + + p.Del[TProps]() + return tmp^ + +fn H[T:ComponentStateless,TProps:CollectionElement]( + owned Props: TProps +)->Element: + var p = Capsule(Props^) + p.type="props" + var tmp = T.Render(p) + p.Del[TProps]() + return tmp^ + +fn H[T:ComponentStateless]( +)->Element: + var p = Capsule() + var tmp = T.Render(p^) + return tmp^ + +trait Html: + @staticmethod + fn tag()->String: ... + +fn H[T:Html]( + *args: + Variant[ + Element, + String, + StringLiteral, + CSS_T, + EventHandler + ], + **kwargs: String +) -> Element: + var tmp = Element(T.tag()) + tmp.component = None + for k in kwargs: + try:tmp.attributes[k[]]=kwargs[k[]] + except e:print(e) + if len(args)>0: + for i in range(len(args)): + var arg = args[i] + if arg.isa[Element](): + tmp.inner.append(arg.take[Element]()) + if arg.isa[String](): + var tmp3 = Element("TextNode") + tmp3.attributes["value"] = arg.take[String]() + tmp.inner.append(tmp3) + if arg.isa[StringLiteral](): + var tmp3 = Element("TextNode") + tmp3.attributes["value"] = arg.take[StringLiteral]() + tmp.inner.append(tmp3) + if arg.isa[EventHandler](): + var tmp_arg = arg.take[EventHandler]() + tmp.attributes[String("data-event_")+tmp_arg.type]= + tmp_arg.event_name + if arg.isa[CSS_T](): + tmp.attributes["style"]=arg.take[CSS_T]().value + return tmp +@value +struct Element: + var component: Optional[Component_T] + var tag: String + var inner:List[Variant[Self,NoneType]] + var attributes: Dict[String,String] + + fn __init__(inout self,tag:String): + self.component = None + self.tag=tag + self.inner=List[Variant[Self,NoneType]]() + self.attributes = Dict[String,String]() + + fn __copyinit__(inout self,other:Self): + self.component = other.component + self.tag= other.tag + self.inner = other.inner + self.attributes = other.attributes + fn __moveinit__(inout self,owned other:Self): + self.component = other.component + self.tag= other.tag + self.inner = other.inner + self.attributes = other.attributes + + +@value +struct Div: + @staticmethod + fn tag()->String: return "Div" +@value +struct H1: + @staticmethod + fn tag()->String: return "H1" +@value +struct H2: + @staticmethod + fn tag()->String: return "H1" +@value +struct A: + @staticmethod + fn tag()->String: return "a" +@value +struct Span: + @staticmethod + fn tag()->String: return "span" +@value +struct Button: + @staticmethod + fn tag()->String: return "button" + +@value +struct Br: + @staticmethod + fn tag()->String: return "br" + +@value +struct Input: + @staticmethod + fn tag()->String: return "input" + + + + + diff --git a/struct composability prototype/Helpers.mojo b/struct composability prototype/Helpers.mojo new file mode 100644 index 0000000..9d97c58 --- /dev/null +++ b/struct composability prototype/Helpers.mojo @@ -0,0 +1,30 @@ +from utils.variant import Variant + +#f["hello {message} {emoji}"](message="world", emoji="🔥") +fn f[fmt:StringLiteral](**kwargs:String) ->String: + var res = String(fmt) + try: + for k in kwargs: + if(kwargs[k[]].count("{")+kwargs[k[]].count("}"))!=0: + raise Error("simple safeguard: remplacement value could contains {}") + try: res=res.replace("{"+k[]+"}",kwargs[k[]]) + except e: + res = "Error raised by f" #TODO: better + raise e + except e: print("f formatting function:",e) + return res + + +#print(CSS(color="red")) +@value +struct CSS_T: + var value:String + +fn CSS(**args:String)->CSS_T: + var res = CSS_T("") + + for a in args: + try: res.value+=a[]+":"+args[a[]]+";" + except e: print("CSS function, render == True :",e) + + return res diff --git a/struct composability prototype/README.md b/struct composability prototype/README.md new file mode 100644 index 0000000..c758d96 --- /dev/null +++ b/struct composability prototype/README.md @@ -0,0 +1,300 @@ + +# Integration prototype 🔥❤️🐍 + +> [!NOTE] +> This prototype was made to ameliorate design of LSX. +> It should not be used for anything in production (lots of UnsafePointer). + + +Hope this will be useful to people that wan't to make web-frameworks too + +Lukas is working on lsx, the goal is to use 100% lsx with a wrapper on top if necessary + +Currently, it's just prototyping with the idea to see what to do + +# ⚠️ +- Start a socket, http-server: ```ip: 127.0.0.1 port: 8080``` +- Probably many unfreed pointers (need help) +- No current support for multiple clients/visitors, a session system would be neededdd + +# Why +- By having another use case for the design, we can identify what could be done in order to ameliorate it. +- Its fun to program in mojo +- further the development of web-frameworks + +# What is done so far: +- Reusable components +- Stateless components +- init and deinit on mount and unmount +- Rendering from a tree on the client-side with JS +- Basic event passing to instances of components +- Props + +# Challenges +- Instance name cannot use underscores (don't know why) +- Probably more things + +# Current conclusions +- LSX is a nice and expressive way to build a DOM, deserves attention and work + - great degree of composability + - very user-friendly (**kwargs and *args, for example) +- Mojo is awesome + +# Available features that could bring more to LSX: + - functions with same name and different signature ! + +# Possible feature request: + - ```__struct_name_of[App]() -> StringLiteral: "App"``` (struct name) + +# Example: +```mojo +from Helpers import * +from DomEvent import * +from DomTree import * +from Component import * +from ComponentManager import * +from Server import * +from css import * + +alias m_ = ComponentTypes[ + App, + Menu, + About, + Home, + CounterWidget, + TodoList +] + +var m=ComponentManager[ + m_.Ts +]() + +fn main(): + + var s = Server() + s.extra_css = spin_css + + s.Start[App]() + + for i in range(20): + s.ProcessRequest[App]() + + s^.Stop() + + + +@value +struct App(Component): + @staticmethod + fn component_name()->String: return "App" + var title:String + var page:String + + fn __init__(inout self): + self.page = "home" + self.title="Default app title" + + fn Event(inout self , e:DomEvent): + if e.name == "title_edited": + self.title=e.data + + @staticmethod + fn Render(inout self, owned props:Capsule) -> Element: + var tmp = H[Div]() + if self.page == "about": + tmp = H[About]("aboutinstance") + if self.page == "home": + tmp = H[Home]("homeinstance") + + return + H[Div]( + CSS( + width="fit-content", + ), + H[H1](self.title,CSS(`background-color`="deeppink")), + H[Input]( + On[DomEvent.Change]("title_edited"), + value=self.title, + type="text" + ), + H[H1]( + "🐣", + CSS( + animation="spin 2s infinite", + width="fit-content", + `font-size`="128px", + margin="0" + ) + ), + H[Menu]("menuinstance"), + tmp, + H[CounterWidget]("CounterA",String("Increment")), + H[CounterWidget]("CounterB",String("Increment")), + H[Br](),H[Br](), + H[HelloWorld](), + H[HelloWorld](self.title), + + ) +@value +struct Menu(Component): + @staticmethod + fn component_name()->String: return "Menu" + + + fn __init__(inout self): + ... + + + fn Event(inout self , e:DomEvent): + var i = m.InstanceOf["AppInstance"]() + if i: + if e.name == "aboutevent": + i[App][].page="about" + if e.name == "homeevent": + i[App][].page="home" + + + fn Render(inout self, owned props:Capsule) -> Element: + var tmp = CSS(height=32,margin=16) + return + H[Div]( + H[Button]( + "About", + On[DomEvent.Click]("aboutevent"), + tmp + ), + + H[Button]( + "Home", + On[DomEvent.Click]("homeevent"), + tmp + ) + ) +@value +struct About(Component): + @staticmethod + fn component_name()->String: return "About" + + var counter: Int + fn __init__(inout self): + self.counter = 0 + + fn Event(inout self , e:DomEvent): + ... + + @staticmethod + fn Render(inout self, owned props:Capsule) -> Element: + return + H[Div]( + CSS( + `background-color`="floralwhite", + color="aquamarine" + ), + H[H1]( + "about page" + ), + H[Span]("count :"+str(self.counter)) + ) +@value +struct Home(Component): + @staticmethod + fn component_name()->String: return "Home" + + + fn __init__(inout self): ... + + + fn Event(inout self , e:DomEvent): + ... + + fn Render(inout self, owned props:Capsule) -> Element: + return + H[Div]( + CSS( + `background-color`="yellow", + color="blue", padding=4 + ), + H[Span]( + "home page" + ), + H[TodoList]("TodoList1"), + H[TodoList]("TodoList2") + ) + +@value +struct CounterWidget(Component): + @staticmethod + fn component_name()->String: return "CounterWidget" + + var counter: Int + fn __init__(inout self): + self.counter = 0 + + fn Event(inout self , e:DomEvent): + var p = m.InstanceOf["AppInstance"]() + if e.name == "change_app_title" and p: + self.counter+=1 + p[App][].title= + str(self.counter) + + fn Render(inout self, owned props:Capsule) -> Element: + var tmp = CSS( + margin=8,`background-color`="skyblue", + `font-size`=16+self.counter + ) + + if props: + return H[Button]( + props[String][], + On[DomEvent.Click]("change_app_title"), + tmp + ) + return H[Button]("Default",tmp) + +struct HelloWorld(ComponentStateless): + @staticmethod + fn Render(owned Props:Capsule)->Element: + if Props: + return H[Div]( + H[Span]("props:"), + H[Span](Props[String][]) + ) + else: + return H[Div]( + H[Span]("Hello world") + ) + +@value +struct TodoList(Component): + @staticmethod + fn component_name()->String: return "todolist" + + var todos: List[String] + var editor: String + + fn __init__(inout self): + self.todos= List[String]() + self.editor = "ok" + + fn Event(inout self, e:DomEvent): + if e.name == "editor_edited": self.editor=e.data + if e.name == "add_todo": self.todos.append(self.editor) + + fn Render(inout self,owned Props:Capsule)->Element: + var tmp = H[Div]( + CSS(`background-color`="white",`font-size`=16,color="black"), + ) + for t in self.todos: tmp.inner.append( + H[Div](t[]) + ) + return H[Div]( + CSS(margin=4), + H[Input]( + On[DomEvent.Change]("editor_edited"), + value=self.editor, + type="text" + ), + H[Button]("Add",On[DomEvent.Click]("add_todo")), + tmp + ) +``` \ No newline at end of file diff --git a/struct composability prototype/Server.mojo b/struct composability prototype/Server.mojo new file mode 100644 index 0000000..298fda8 --- /dev/null +++ b/struct composability prototype/Server.mojo @@ -0,0 +1,145 @@ +from ComponentManager import * +from python import Python +from app import m,m_ + +@value +struct Server: + var socket:PythonObject + var js_renderer: String + var extra_css:String + var url:String + fn __init__(inout self): + self.socket=None + try: + with open("js_renderer.js","r") as f: + self.js_renderer = f.read() + except e: + print("Error importing js_renderer.js: " + str(e)) + self.js_renderer = "" + #TODO: set self.error = True + self.extra_css = " " + self.url = "" + + fn ProcessRequest[App_T:Component](inout self): + print("ok") + try: + var to_json = Python.import_module("json").loads + print("request loop-----------------") + var client = self.socket.accept() + if client[1][0] != '127.0.0.1': + print("Exit, request from: "+str(client[1][0])) + return + + var request = client[0].recv(1024).decode() + var full_request = request.split('\n') + request = request.split('\n')[0].split(" ") + if request[1]=="/": + var response:String = "HTTP/1.0 200 OK\n\n" + response += "" + response += "" + response += "" + response += "" + response+="" + client[0].sendall(PythonObject(response).encode()) + client[0].close() + elif request[1]=="/styles.css": + var response:String = "HTTP/1.0 200 OK\n\n" + response += self.extra_css + client[0].sendall(PythonObject(response).encode()) + client[0].close() + elif request[1]=="/event": + var json_req = to_json( + full_request[-1] + ) + var response:String = "HTTP/1.0 200 OK\n\n" + + if str(json_req["type"]) == "DomEvent": + #print(json_req) + var instance_name = str(json_req["instance_name"]) + var event_name = str(json_req["event_name"]) + var event_data = str(json_req["data"]) + print(json_req) + for instance in m.states: + if instance[].instance_name==instance_name: + + @parameter + fn _loop[I:Int](): + if is_component[m_.Ts[I]](instance[].type): + var tmp_capsule:Capsule = instance[] + var tmp_i = move_from_pointee(tmp_capsule.value.bitcast[m_.Ts[I]]()) + + + + var ref:UnsafePointer[m_.Ts[I]] = tmp_capsule[m_.Ts[I]] + m_.Ts[I].Event( + tmp_i , + DomEvent( + "", + "type", + event_name, + event_data, + ) + ) + initialize_pointee_move(tmp_capsule.value.bitcast[m_.Ts[I]](),tmp_i^) + + unroll[_loop,len(VariadicList(m_.Ts))]() + + _=event_name + _=instance_name + _=event_data + + for s in m.states: + s[].rendered=False + + var elements = H[App_T]("AppInstance") + + response += m.RenderIntoJson[ + True + ](elements) + + m.delete_instances[ + only_unrendered=True + ]() + + print("self.states:") + for i in m.states: + print("\t",i[].instance_name) + #print("\t\t",i[].value) + + client[0].sendall(PythonObject(response).encode()) + client[0].close() + except e: print(e) + + fn Stop(owned self): + m.delete_instances() + try:self.socket.close() + except e:print(e) + Self.__del__(self^) + + fn Start[App_T:Component](inout self): + constrained[ + TsCount[App_T,m_.Ts](), + "fn Start[App_T:Component](inout self), App_T not in m_.Ts" + ]() + try: + var socket = Python.import_module("socket") + var r = Reference(self.socket) + r[] = socket.socket( + socket.AF_INET, + socket.SOCK_STREAM + ) + r[].setsockopt( + socket.SOL_SOCKET, + socket.SO_REUSEADDR, + 1 + ) + var host = "127.0.0.1" + var port = 8080 + self.socket.bind((host, port)) + self.socket.listen(1) + + self.url = "http://"+str(host)+":"+port + print(self.url) + + except e: + print(e) diff --git a/struct composability prototype/app.mojo b/struct composability prototype/app.mojo new file mode 100644 index 0000000..a577c31 --- /dev/null +++ b/struct composability prototype/app.mojo @@ -0,0 +1,250 @@ +from Helpers import * +from DomEvent import * +from DomTree import * +from Component import * +from ComponentManager import * +from Server import * +from css import * + +alias m_ = ComponentTypes[ + App, + Menu, + About, + Home, + CounterWidget, + TodoList +] + +var m=ComponentManager[ + m_.Ts +]() + +fn main(): + + var s = Server() + s.extra_css = spin_css + + s.Start[App]() + + for i in range(20): + s.ProcessRequest[App]() + + s^.Stop() + + + +@value +struct App(Component): + @staticmethod + fn component_name()->String: return "App" + var title:String + var page:String + + fn __init__(inout self): + self.page = "home" + self.title="Default app title" + + fn Event(inout self , e:DomEvent): + if e.name == "title_edited": + self.title=e.data + + @staticmethod + fn Render(inout self, owned props:Capsule) -> Element: + var tmp = H[Div]() + if self.page == "about": + tmp = H[About]("aboutinstance") + if self.page == "home": + tmp = H[Home]("homeinstance") + + return + H[Div]( + CSS( + width="fit-content", + ), + H[H1](self.title,CSS(`background-color`="deeppink")), + H[Input]( + On[DomEvent.Change]("title_edited"), + value=self.title, + type="text" + ), + H[H1]( + "🐣", + CSS( + animation="spin 2s infinite", + width="fit-content", + `font-size`="128px", + margin="0" + ) + ), + H[Menu]("menuinstance"), + tmp, + H[CounterWidget]("CounterA",String("Increment")), + H[CounterWidget]("CounterB",String("Increment")), + H[Br](),H[Br](), + H[HelloWorld](), + H[HelloWorld](self.title), + + ) +@value +struct Menu(Component): + @staticmethod + fn component_name()->String: return "Menu" + + + fn __init__(inout self): + ... + + + fn Event(inout self , e:DomEvent): + var i = m.InstanceOf["AppInstance"]() + if i: + if e.name == "aboutevent": + i[App][].page="about" + if e.name == "homeevent": + i[App][].page="home" + + + fn Render(inout self, owned props:Capsule) -> Element: + var tmp = CSS(height=32,margin=16) + return + H[Div]( + H[Button]( + "About", + On[DomEvent.Click]("aboutevent"), + tmp + ), + + H[Button]( + "Home", + On[DomEvent.Click]("homeevent"), + tmp + ) + ) +@value +struct About(Component): + @staticmethod + fn component_name()->String: return "About" + + var counter: Int + fn __init__(inout self): + self.counter = 0 + + fn Event(inout self , e:DomEvent): + ... + + @staticmethod + fn Render(inout self, owned props:Capsule) -> Element: + return + H[Div]( + CSS( + `background-color`="floralwhite", + color="aquamarine" + ), + H[H1]( + "about page" + ), + H[Span]("count :"+str(self.counter)) + ) +@value +struct Home(Component): + @staticmethod + fn component_name()->String: return "Home" + + + fn __init__(inout self): ... + + + fn Event(inout self , e:DomEvent): + ... + + fn Render(inout self, owned props:Capsule) -> Element: + return + H[Div]( + CSS( + `background-color`="yellow", + color="blue", padding=4 + ), + H[Span]( + "home page" + ), + H[TodoList]("TodoList1"), + H[TodoList]("TodoList2") + ) + +@value +struct CounterWidget(Component): + @staticmethod + fn component_name()->String: return "CounterWidget" + + var counter: Int + fn __init__(inout self): + self.counter = 0 + + fn Event(inout self , e:DomEvent): + var p = m.InstanceOf["AppInstance"]() + if e.name == "change_app_title" and p: + self.counter+=1 + p[App][].title= + str(self.counter) + + fn Render(inout self, owned props:Capsule) -> Element: + var tmp = CSS( + margin=8,`background-color`="skyblue", + `font-size`=16+self.counter + ) + + if props: + return H[Button]( + props[String][], + On[DomEvent.Click]("change_app_title"), + tmp + ) + return H[Button]("Default",tmp) + +struct HelloWorld(ComponentStateless): + @staticmethod + fn Render(owned Props:Capsule)->Element: + if Props: + return H[Div]( + H[Span]("props:"), + H[Span](Props[String][]) + ) + else: + return H[Div]( + H[Span]("Hello world") + ) + +@value +struct TodoList(Component): + @staticmethod + fn component_name()->String: return "todolist" + + var todos: List[String] + var editor: String + + fn __init__(inout self): + self.todos= List[String]() + self.editor = "ok" + + fn Event(inout self, e:DomEvent): + if e.name == "editor_edited": self.editor=e.data + if e.name == "add_todo": self.todos.append(self.editor) + + fn Render(inout self,owned Props:Capsule)->Element: + var tmp = H[Div]( + CSS(`background-color`="white",`font-size`=16,color="black"), + ) + for t in self.todos: tmp.inner.append( + H[Div](t[]) + ) + return H[Div]( + CSS(margin=4), + H[Input]( + On[DomEvent.Change]("editor_edited"), + value=self.editor, + type="text" + ), + H[Button]("Add",On[DomEvent.Click]("add_todo")), + tmp + ) + \ No newline at end of file diff --git a/struct composability prototype/css.mojo b/struct composability prototype/css.mojo new file mode 100644 index 0000000..aa30451 --- /dev/null +++ b/struct composability prototype/css.mojo @@ -0,0 +1,16 @@ +alias spin_css=""" + body { + text-align: -moz-center; + font-family: monospace; + backgroung-color: teal; + } + + @keyframes spin { + from { + transform:rotate(0deg); + } + to { + transform:rotate(360deg); + } + } + """ \ No newline at end of file diff --git a/struct composability prototype/js_renderer.js b/struct composability prototype/js_renderer.js new file mode 100644 index 0000000..9eb13ee --- /dev/null +++ b/struct composability prototype/js_renderer.js @@ -0,0 +1,78 @@ +async function Event(data) { + const response = await fetch("/event", { + method: "POST", + headers: { "Content-Type": "application/json",}, + body: JSON.stringify(data), + }); + + const json = await response.json(); + var render_res = Render(json,json["instance_name"]) + var b = document.createElement("BODY") + b.appendChild(render_res) + document.body=b + +} +window.addEventListener("load", (event) => { + const data = { type: "first render" }; + Event(data); +}); + +function Render(json,instance_name){ + var instance_n = instance_name + if (json.hasOwnProperty("data-instance_name")){ + instance_n = json["data-instance_name"] + } + var tag = json["tag"] + if (tag == "TextNode"){ + return document.createTextNode(json["value"]); + } + var inner = json["inner"] + delete json["inner"]; + var el = document.createElement(tag); + for (attr in json){ + if (attr.startsWith("data-event_")){ + console.log(attr,json[attr]) + var ev = attr.split("data-event_")[1] + var ev_val = json[attr] + if (ev == "Click"){ + el.addEventListener("click",(e)=>{ + Event({ + type: "DomEvent" , + event_name: ev_val, + instance_name: instance_n, + data:"" + }); + }) + } + if (ev == "MouseEnter"){ + el.addEventListener("mouseenter",(e)=>{ + console.log(ev_val) + Event({ + type: "DomEvent" , + event_name: ev_val, + instance_name: instance_n, + data:"" + }); + }) + } + if (ev == "Change"){ + el.addEventListener("change",(e)=>{ + console.log(ev_val,e.target.value) + Event({ + type: "DomEvent" , + event_name: ev_val, + instance_name: instance_n, + data: e.target.value + }); + }) + } + } + el.setAttribute(attr,json[attr]) + } + inner.forEach((inner_el) => { + el.appendChild(Render(inner_el,instance_n)) + }); + return el + +} + diff --git a/struct composability prototype/todo.md b/struct composability prototype/todo.md new file mode 100644 index 0000000..fc737fa --- /dev/null +++ b/struct composability prototype/todo.md @@ -0,0 +1,2 @@ +- CSS_T + CSS_T +- On[DomEvent.Click]("Event_name","123") \ No newline at end of file