diff --git a/src/app.js b/src/app.js
deleted file mode 100644
index 2813c93..0000000
--- a/src/app.js
+++ /dev/null
@@ -1,19 +0,0 @@
-
-export class TurtleApp{
- constructor(root){
- this.root = root
- this.data = {}
- this.components = {}
- this.modules = []
- }
-
- use(module,configs ={}){
- this.modules.push(module.init(this,configs))
- }
-
-}
-
-export function createApp(root) {
- return new TurtleApp(root)
-}
-
diff --git a/src/app/index.js b/src/app/index.js
new file mode 100644
index 0000000..e5f400b
--- /dev/null
+++ b/src/app/index.js
@@ -0,0 +1,31 @@
+import {render} from '../dom/render.js';
+
+export class TurtleApp {
+ constructor(root) {
+ this.root = root
+ this.data = {}
+ this.modules = []
+ this.html = render.bind(this)
+ }
+
+ use(module,configs) {
+ this.modules.forEach(m => {
+ if (m === module) throw `Module already exists in this app !`
+ })
+
+ if (!module.init) {
+ throw `Cannot find init function of module `
+ } else {
+ module.init(this,configs)
+ this.modules.push(module)
+ }
+ }
+
+ render(content) {
+ this.root.appendChild(content.root)
+ }
+}
+
+export function createApp(rootElement) {
+ return new TurtleApp(rootElement)
+}
\ No newline at end of file
diff --git a/src/components/component.js b/src/component/index.js
similarity index 55%
rename from src/components/component.js
rename to src/component/index.js
index bfe27d2..bdb3355 100644
--- a/src/components/component.js
+++ b/src/component/index.js
@@ -1,32 +1,51 @@
-import { render } from './render.js';
-import {TurtleComponentState} from './state.js';
+import { render } from '../dom/render.js';
+import { createState } from './state.js';
+
function evalInScope(js, contextAsScope) {
return new Function(`return ${js}`).call(contextAsScope);
}
-export class TurtleComponent {
+class TurtleComponentElement extends HTMLElement {
+ constructor() {
+ super()
+ this._controller = null
+ }
+
+ connectedCallback() {
+ this._controller.start(this)
+ }
+}
+
+window.customElements.define("turtle-component", TurtleComponentElement)
+
+export class TurtleComponentInstance {
constructor(fn, props) {
- this._fn = fn.bind(this)
- this._props = props
- this._base = null
+ this.fn = fn
+ this.props = props
+ this.data = {}
+ this._memories = {}
this._refs = {}
+ this._element = null
this._reactive = true
- this._memories = []
this.html = render.bind(this)
this.onCreate = new Function()
this.onRender = new Function()
this.onUpdate = new Function()
this.onDestroy = new Function()
}
-
- createState(value){
- return new TurtleComponentState(this,value)
+
+ initState(value) {
+ let state = createState(value)
+ state.component = this
+ return state
}
- get refs(){
- return this._refs
+ addUpdateDependents(dependents) {
+ dependents.forEach(dependent => {
+ dependent.component = this
+ })
}
-
+
forceUpdate() {
for (let i = 0; i < this._memories.length; i++) {
let d = this._memories[i]
@@ -45,14 +64,18 @@ export class TurtleComponent {
d.node.innerHTML = evalInScope(d.expr, this)
}
}
-
+
this.onUpdate()
}
- start() {
- this.onCreate()
- this._base._c = this
- this._base.appendChild(this._fn(this,...this._props))
+ start(element) {
+ this._element = element
+ let { root, context } = this.requestRender()
+
+ this._element.appendChild(root)
+ this._memories = context._memories
+ this._refs = context._refs
+
for (let i = 0; i < this._memories.length; i++) {
let d = this._memories[i]
if (d.type == "attr") {
@@ -70,15 +93,22 @@ export class TurtleComponent {
d.node.innerHTML = evalInScope(d.expr, this)
}
}
+
this.onRender()
+
+ }
+
+ requestRender() {
+ return this.fn.bind(this)(this, ...this.props)
}
-
+
}
export function createComponent(fn) {
let fn_component = function(...props) {
- return new TurtleComponent(fn, props)
+ return new TurtleComponentInstance(fn, props)
}
- fn_component.instance = TurtleComponent
+
+ fn_component.instance = TurtleComponentInstance
return fn_component
}
\ No newline at end of file
diff --git a/src/component/state.js b/src/component/state.js
new file mode 100644
index 0000000..31ca5b1
--- /dev/null
+++ b/src/component/state.js
@@ -0,0 +1,24 @@
+import { TurtleComponentInstance } from './index.js';
+
+class TurtleComponentState {
+ constructor(component, value) {
+ this.component = component
+ this._value = value
+ }
+
+ get value() {
+ return this._value
+ }
+
+ set value(val) {
+ this._value = val
+ if (this.component instanceof TurtleComponentInstance) {
+ if (!this.component._reactive) return
+ this.component.forceUpdate()
+ }
+ }
+}
+
+export function createState(value) {
+ return new TurtleComponentState(null, value)
+}
\ No newline at end of file
diff --git a/src/components/base.js b/src/components/base.js
deleted file mode 100644
index 844c9d6..0000000
--- a/src/components/base.js
+++ /dev/null
@@ -1,17 +0,0 @@
-export class TurtleBaseComponent extends HTMLElement {
- constructor() {
- super()
- this._controller = null
- this._c = null
- }
-
- connectedCallback(){
- this._controller(this)
- }
-
- disconnectedCallback(){
- this._c.onDestroy()
- }
-}
-
-window.customElements.define("turtle-component",TurtleBaseComponent)
\ No newline at end of file
diff --git a/src/components/processing.js b/src/components/processing.js
deleted file mode 100644
index 58cc4c1..0000000
--- a/src/components/processing.js
+++ /dev/null
@@ -1,75 +0,0 @@
-import {TurtleComponent} from './component.js';
-
-function checkAndExtractId(inputString) {
- const regex = /^TurtleComponent_([a-zA-Z0-9]+)$/;
- const match = inputString.match(regex);
-
- if (match) {
- const id = match[1];
- return id;
- } else {
- return null;
- }
-}
-
-export function processAttributes(node, root, context = {}) {
- if (node && root) {
- for (var i = 0; i < node.attributes.length; i++) {
- let attribute = node.attributes[i];
- if (attribute.localName.indexOf('bind-') === 0) {
- let attrName = attribute.localName.substring('bind-'.length);
- if (context.root instanceof TurtleComponent) {
- context.root._memories.push({ type: "attr", attr: attrName, node: root, expr: attribute.value })
- root.setAttribute(attrName, attribute.value);
- }
- } else if (attribute.localName.indexOf('on-') === 0) {
- let eventName = attribute.localName.substring('on-'.length);
- root.addEventListener(eventName, evalInScope(attribute.value, context))
- } else if (attribute.localName == "t-text") {
- if (context.root instanceof TurtleComponent) {
- context.root._memories.push({ type: "text", node: root, expr: attribute.value })
- }
- } else if (attribute.localName == "t-ref") {
- context.root._refs[attribute.value] = root
- } else if (attribute.localName == "t-html") {
- if (context.root instanceof TurtleComponent) {
- context.root._memories.push({ type: "html", node: root, expr: attribute.value })
- }
- } else {
- root.setAttribute(attribute.name, attribute.value);
- }
- }
- }
-}
-
-
-export function processing(dom, contents, context) {
- for (let i = 0; i < contents.childNodes.length; i++) {
- let node = contents.childNodes[i]
-
- if (node.nodeType === Node.ELEMENT_NODE) {
- let name = node.tagName
- let element = document.createElement(node.tagName)
- if (element instanceof HTMLUnknownElement) {
- if (name.indexOf("TurtleComponent_") != -1) {
- let id = checkAndExtractId(name)
- if (id) {
- if (context.data.components[id]) {
- element = document.createElement("turtle-component")
- element.setAttribute("id", id)
- element._controller = context.data.components[id]
- dom.appendChild(element)
- }
- }
- }
- } else {
- processAttributes(node, element, context)
- processing(element, node, context)
- dom.appendChild(element)
- }
- } else if (node.nodeType === Node.TEXT_NODE) {
- dom.appendChild(document.createTextNode(node.textContent))
- }
- }
-
-}
\ No newline at end of file
diff --git a/src/components/render.js b/src/components/render.js
deleted file mode 100644
index c28b4c5..0000000
--- a/src/components/render.js
+++ /dev/null
@@ -1,52 +0,0 @@
-import { TurtleComponent } from './component.js';
-import { processing } from './processing.js';
-
-
-export function render(str, ...values) {
- let data = {
- components: {}
- }
-
- for (let i = 0; i < values.length; i++) {
- let value = values[i]
- if (value instanceof TurtleComponent) {
- let key = (Math.floor(Math.random() * 9999) * Date.now()).toString(16)
- data.components[key] = function(base) {
- value._base = base
- return value.start()
- }
-
- values[i] = `TurtleComponent_${key}`
- }
-
- if (value) {
- if (value.instance === TurtleComponent) {
- let key = (Math.floor(Math.random() * 9999) * Date.now()).toString(16)
- value = value()
- data.components[key] = function(base) {
- value._base = base
- return value.start()
- }
- values[i] = `TurtleComponent_${key}`
-
- }
- }
- }
-
- let content = new DOMParser().parseFromString(
- `${String.raw(str,...values)}`,
- "text/xml"
- ).querySelector("root")
-
- let fragment = document.createDocumentFragment()
- processing(
- fragment,
- content,
- {
- data: data,
- root: this
- }
- )
-
- return fragment
-}
\ No newline at end of file
diff --git a/src/components/state.js b/src/components/state.js
deleted file mode 100644
index 7025268..0000000
--- a/src/components/state.js
+++ /dev/null
@@ -1,17 +0,0 @@
-export class TurtleComponentState{
- constructor(component,value){
- this._component = component
- this._value = value
- }
-
- set val(value){
- this._value = value
- if(this._component._reactive){
- this._component.forceUpdate()
- }
- }
-
- get val(){
- return this._value
- }
-}
\ No newline at end of file
diff --git a/src/dom/attributes.js b/src/dom/attributes.js
new file mode 100644
index 0000000..34cd79c
--- /dev/null
+++ b/src/dom/attributes.js
@@ -0,0 +1,27 @@
+function evalInScope(js, contextAsScope) {
+ return new Function(`return ${js}`).call(contextAsScope);
+}
+
+export function processAttributes(node, root, context = {}, scope) {
+ if (node && root) {
+ for (var i = 0; i < node.attributes.length; i++) {
+ let attribute = node.attributes[i];
+ if (attribute.localName.indexOf('bind-') === 0) {
+ let attrName = attribute.localName.substring('bind-'.length);
+ context._memories.push({ type: "attr", attr: attrName, node: root, expr: attribute.value })
+ root.setAttribute(attrName, attribute.value);
+ } else if (attribute.localName.indexOf('on-') === 0) {
+ let eventName = attribute.localName.substring('on-'.length);
+ root.addEventListener(eventName, evalInScope(attribute.value, scope))
+ } else if (attribute.localName == "t-text") {
+ context._memories.push({ type: "text", node: root, expr: attribute.value })
+ } else if (attribute.localName == "t-ref") {
+ context._refs[attribute.value] = root
+ } else if (attribute.localName == "t-html") {
+ context._memories.push({ type: "html", node: root, expr: attribute.value })
+ } else {
+ root.setAttribute(attribute.name, attribute.value);
+ }
+ }
+ }
+}
diff --git a/src/dom/memorize.js b/src/dom/memorize.js
new file mode 100644
index 0000000..09debb3
--- /dev/null
+++ b/src/dom/memorize.js
@@ -0,0 +1,7 @@
+export class TurtleMemorizeContext{
+ constructor(component){
+ this.component = component
+ this._memories = []
+ this._refs = {}
+ }
+}
\ No newline at end of file
diff --git a/src/dom/parser.js b/src/dom/parser.js
new file mode 100644
index 0000000..b36d547
--- /dev/null
+++ b/src/dom/parser.js
@@ -0,0 +1,7 @@
+export function parserContent(content) {
+ content = new DOMParser().parseFromString(
+ `${content}`,
+ "text/xml"
+ ).querySelector("root")
+ return content
+}
\ No newline at end of file
diff --git a/src/dom/ref.js b/src/dom/ref.js
new file mode 100644
index 0000000..e69de29
diff --git a/src/dom/render.js b/src/dom/render.js
new file mode 100644
index 0000000..fb6d0bd
--- /dev/null
+++ b/src/dom/render.js
@@ -0,0 +1,87 @@
+import { TurtleComponentInstance } from '../component/index.js';
+import { TurtleMemorizeContext } from './memorize.js';
+import { parserContent } from './parser.js';
+import {processAttributes} from './attributes.js';
+function checkAndExtractId(inputString) {
+ const regex = /^TurtleComponent_([a-zA-Z0-9]+)$/;
+ const match = inputString.match(regex);
+
+ if (match) {
+ const id = match[1];
+ return id;
+ } else {
+ return null;
+ }
+}
+
+
+
+function processing(root, contents, context, data, scope) {
+ for (let i = 0; i < contents.childNodes.length; i++) {
+ let node = contents.childNodes[i]
+
+ if (node.nodeType === Node.ELEMENT_NODE) {
+ let name = node.tagName
+
+ let element = document.createElement(node.tagName)
+ if (element instanceof HTMLUnknownElement) {
+
+ if (name.indexOf("TurtleComponent_") != -1) {
+ let id = checkAndExtractId(name)
+
+ if (id) {
+ if (data.components[id]) {
+ element = document.createElement("turtle-component")
+ element.setAttribute("id", id)
+ element._controller = data.components[id]
+ root.appendChild(element)
+ }
+ }
+ }
+ } else {
+ processAttributes(node, element, context, scope)
+ processing(element, node, context)
+ root.appendChild(element)
+ }
+ } else if (node.nodeType === Node.TEXT_NODE) {
+ root.appendChild(document.createTextNode(node.textContent))
+ }
+ }
+
+}
+
+export function render(str, ...values) {
+ let renderData = {
+ components: {},
+ }
+
+ for (let i = 0; i < values.length; i++) {
+ let value = values[i]
+ let key = (Math.floor(Math.random() * 99999) * Date.now()).toString(16)
+ if (value instanceof Function && value.instance === TurtleComponentInstance) {
+ renderData.components[key] = value([])
+ values[i] = `TurtleComponent_${key}`
+ }
+
+ if (value instanceof TurtleComponentInstance) {
+ renderData.components[key] = value
+ values[i] = `TurtleComponent_${key}`
+ }
+ }
+
+ let root = document.createDocumentFragment()
+ let context = new TurtleMemorizeContext(this)
+ let content = parserContent(String.raw(str, ...values))
+ processing(root, content, context, renderData,this)
+ return {
+ root,
+ context,
+ content,
+ refs:context._refs
+ }
+}
+
+export function attach(element, content){
+ element.textContent = null
+ element.appendChild(content.root)
+}
\ No newline at end of file
diff --git a/src/modules/router/base.js b/src/modules/router/base.js
deleted file mode 100644
index 3863fc9..0000000
--- a/src/modules/router/base.js
+++ /dev/null
@@ -1,168 +0,0 @@
-import { matches } from './parser.js';
-import { render } from '../../components/render.js';
-export class TurtleRouterModule {
- constructor(app, configs) {
- this.element = configs.element ?? document.createElement("div")
- this.routes = {}
- this.params = {}
- this.events = {
- pagenotfound: [],
- notallow: [],
- routechanged: [],
- loadcontentfailed: []
- }
- this.query = new URLSearchParams()
- this.app = app
- this.matched = null
- app.router = this
- this.currentContext = null
- this.render = render
- this.render = this.render.bind(this)
- }
-
- static init(app, configs) {
- return new TurtleRouterModule(app, configs)
- }
-
- async matches(path) {
- if (path.length == 0) path = "/"
- let matched = false
- let url = new URL(path, window.location.origin)
- path = url.pathname
- if (path.length == 0) path = "/"
- for (let i = 0; i < Object.keys(this.routes).length; i++) {
- let pattern = Object.keys(this.routes)[i]
- let result = matches(pattern, path)
- if (result.matched) {
- if(this.currentContext){
- if(this.currentContext.onRouteChange) this.currentContext.onRouteChange()
- }
- matched = true
- let content_fn = {}
- let route_info = this.routes[pattern]
- if (route_info.beforeLoadContent) route_info.beforeLoadContent.bind(this)()
- this.matched = pattern
- this.params = result.params
- this.url = path
- if (route_info.protect) {
- let res = await route_info.protect()
- if (!res) {
- this.triggerEvent("notallow")
- return
- }
- }
- this.query = url.searchParams
- if (route_info.loader) content_fn = await route_info.loader.bind(this)()
- if (route_info.content) content_fn = route_info.content
- if (route_info.onContentLoaded) {
- try {
- route_info.onContentLoaded.bind(this)()
- } catch (err) {
- this.triggerEvent("loadcontentfailed", err)
- }
- }
-
- if (content_fn.template) {
- const context = {
- app: this.app,
- _memories: [],
- _refs: {},
- get refs() {
- return this._refs
- },
-
- }
-
- context.html=render.bind(context)
- this.currentContext = context
- let template = content_fn.template.bind(context)(context, result)
- this.element.textContent = ""
- this.element.appendChild(template)
- if (context.onRender) context.onRender()
- }
-
- if (route_info.callback) { route_info.callback.bind(this)() }
- if (content_fn.configs) {
- let configs = content_fn.configs
- document.title = configs.title ?? document.title
- }
- this.triggerEvent("routechanged")
- break
- }
- }
-
- if (!matched) {
- this.triggerEvent("pagenotfound")
- }
- }
-
- on(name, callback) {
- this.events[name].push(callback)
- }
-
- async redirect(path, replace = false) {
- if (!replace) {
- window.location.hash = path
- } else {
- window.history.replaceState(null, null, `#${path}`)
- path = window.location.hash.slice(1)
- await this.matches(path)
- }
- }
-
- forceDisplayPage(content_fn){
- if (content_fn.template) {
- const context = {
- app: this.app,
- _memories: [],
- _refs: {},
- get refs() {
- return this._refs
- },
-
- }
-
- context.html = render.bind(context)
- this.currentContext = context
- let template = content_fn.template.bind(context)(context, result)
- this.element.textContent = ""
- this.element.appendChild(template)
- if (context.onRender) context.onRender()
- }
- }
-
- triggerEvent(name) {
- this.events[name].forEach(callback => {
- callback()
- })
- }
-
-
- start() {
- let ctx = this
-
- window.addEventListener("hashchange", function(e) {
- let path = window.location.hash.slice(1)
- ctx.matches(path)
- })
-
- window.addEventListener("click", function(event) {
- let target = event.target
- if (target.getAttribute("t-redirect")) {
- event.preventDefault()
- let path = target.getAttribute("t-redirect")
- window.location =`#${path}`
- }
- })
-
- let path = window.location.hash.slice(1)
- this.matches(path)
- }
-
- createPage(fn, configs) {
- return {
- template: fn,
- configs: configs
- }
- }
-}
\ No newline at end of file
diff --git a/src/modules/router/index.js b/src/modules/router/index.js
index b4208ef..160dbc6 100644
--- a/src/modules/router/index.js
+++ b/src/modules/router/index.js
@@ -1 +1,152 @@
-export * from "./base.js"
+import { attach, render } from '../../dom/render.js';
+
+export class TurtleRouterModule {
+ constructor(app, configs) {
+ this.root = configs.root ?? document.createElement("div")
+ this.app = app
+ this.app.router = this
+ this.routes = {}
+ this.matched = null
+ this.url = null
+ this.params = {}
+ this.query = new URLSearchParams()
+ this.events = {
+ notallow: [],
+ notfound: []
+ }
+ }
+
+ on(event, callback) {
+ this.events[event].push(callback)
+ }
+
+ off(event, callback) {
+ this.events[event].forEach((fn, idx) => {
+ if (fn === callback) {
+ this.events[event].splice(idx, 1)
+ }
+ })
+ }
+
+ static init(app, configs) {
+ return new TurtleRouterModule(app, configs)
+ }
+
+ async matches(url) {
+ let u = new URL(url, window.location.origin)
+ url = u.pathname
+ for (let j = 0; j < Object.keys(this.routes).length; j++) {
+ let route = Object.keys(this.routes)[j]
+ let configs = this.routes[route]
+ let routeSplited = route.split("/")
+ let urlSplited = url.split("/")
+ let passed = true
+ let params = {}
+
+ for (let i = 0; i < routeSplited.length; i++) {
+
+ if (urlSplited[i] === undefined) {
+ passed = false
+ break
+ }
+
+ if (routeSplited[i] == "*") {
+ continue
+ }
+
+ if (routeSplited[i][0] == ":") {
+ let name = routeSplited[i].substring(1, routeSplited[i].length)
+ params[name] = urlSplited[i]
+ continue
+ }
+
+ if (routeSplited[i] != urlSplited[i]) {
+ passed = false
+ break
+ }
+ }
+
+ if (passed) {
+ this.params = params
+ this.query = u.searchParams
+ this.matched = route
+ this.url = url
+ let component = new Function()
+ if (configs.callback) { await configs.callback() }
+ if (configs.protect) {
+ let result = await configs.protect()
+ if (!result) {
+ this.triggerError("not_allow")
+ return
+ }
+ }
+
+ if (configs.loader) {
+ component = await configs.loader()
+ }
+
+ if (configs.component) {
+ component = configs.component
+ }
+
+ attach(this.root, render`
+ <${component(this)}/>
+ `)
+ return
+ }
+ }
+
+ this.triggerError("not_found")
+ }
+
+ start() {
+ window.addEventListener("hashchange", function() {
+ let path = window.location.hash
+ if (path.length == 0) {
+ window.location.hash = "#!/"
+ path = "/"
+ } else {
+ path = path.slice(2)
+ }
+ this.matches(path)
+ }.bind(this))
+
+ let path = window.location.hash
+
+ if (path.length == 0) {
+ window.location.hash = "#!/"
+ path = "/"
+ } else {
+ path = path.slice(2)
+ }
+
+ this.matches(path)
+ }
+
+ redirect(path, replace = false) {
+ if (!replace) {
+ window.location.hash = `#!${path}`
+ } else {
+ window.history.replaceState(null, null, `./#!${path}`)
+ this.matches(path)
+ }
+ }
+
+ emitEvent(name, data) {
+ this.events[name].forEach(fn => {
+ fn(data)
+ })
+ }
+
+ triggerError(name) {
+ switch (name) {
+ case 'not_allow':
+ this.emitEvent("notallow", this)
+ break;
+
+ case 'not_found':
+ this.emitEvent("notfound", this)
+ break;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/modules/router/parser.js b/src/modules/router/parser.js
deleted file mode 100644
index 0a9067d..0000000
--- a/src/modules/router/parser.js
+++ /dev/null
@@ -1,28 +0,0 @@
-
-export function matches(pattern, urlToMatch) {
- const patternParts = pattern.split('/');
- const urlParts = urlToMatch.split('/');
-
- if (patternParts.length !== urlParts.length) {
- return { matched: false };
- }
-
- const params = {};
-
- for (let i = 0; i < patternParts.length; i++) {
- const patternPart = patternParts[i];
- const urlPart = urlParts[i];
-
- if (patternPart.startsWith(':')) {
- const paramName = patternPart.slice(1);
- params[paramName] = urlPart;
- } else if (patternPart === '*') {
- continue
- } else if (patternPart !== urlPart) {
- return { matched: false };
- }
- }
-
- return { matched: true, params };
-}
-
diff --git a/src/turtle.js b/src/turtle.js
index 30de559..977c213 100644
--- a/src/turtle.js
+++ b/src/turtle.js
@@ -1,10 +1,9 @@
-import {} from './components/base.js';
-export * from "./components/component.js"
-export { render } from "./components/render.js"
+export {render as fragment} from './dom/render.js';
+export * from "./component/index.js"
+export * from "./component/state.js"
+export * from "./app/index.js"
export * from "./modules/router/index.js"
-export * from "./app.js"
-
-export function addScript(src, asyncLoad=false,script) {
+export function addScript(src, asyncLoad = false, script) {
let d = document
return new Promise((resolve, reject) => {
script = d.createElement('script');