Closed clsource closed 4 years ago
Hi @clsource, thanks for this issue!
I feel your need here and I wish to add support for Javascript Core. It is possible to make the elementBuilders
variable a global variable available on windows.elementBuilders
so it does not have to be imported, if import
is not supported. I could do that.
But I fear that it still won't work because from the research I have done it is difficult or not really possible to get ES6 Proxy to work in ES5, which is needed in Webscript. Do you know if there is a solution in/with Javascript Core to get Proxy to work on it?
I can make some tests. ES6 features are supported. Only the import part is difficult to replicate. If the library can work using window global then I think that would work without ES5 transpilation 👍
Sounds good. I added the global variable. When Webscript is not being used as an ES6 module you can now access elementBuilders
in the following way:
const { elementBuilders } = window.Webscript
Please test it out to ensure it works.
Ok it worked. But since Javascript Core is a pure JS execution environment. the keywordswindow
, document
and export
were not available. So I have to modify the script to this
(function(){
// @ts-check
function addChild(element, child) {
if (typeof child === "number"
|| typeof child === "bigint"
|| typeof child === "boolean"
|| child instanceof Date
|| child instanceof RegExp) {
element.append(String(child))
}
else if (Array.isArray(child)) {
for (const childChild of child) {
addChild(element, childChild);
}
}
else if (typeof child !== "undefined" && child !== null) {
element.append(child);
}
}
function createElement(tagName, props, ...children) {
tagName = tagName.toLowerCase();
const element = {tagName};
element.children = [];
element.append = (child) => {
element.children.push(child);
};
for (let key in props) {
const value = props[key];
if (typeof value === "string") {
if (key === "className") {
key = "class"
}
}
element[key] = value;
}
for (const child of children) {
addChild(element, child);
}
return element;
}
function templateValues(args) {
const [strings, ...templateArgs] = args;
const result = [];
for (const [index, s] of strings.entries()) {
if (s !== "") {
result.push(s);
}
let arg = templateArgs[index];
if (typeof arg !== "undefined") {
result.push(arg)
}
}
return result
}
function elementBuilderBuilder(elementConstructor, element) {
function getPropertyValue(...args) {
let [first] = args;
if (typeof first === "undefined") {
first = '';
}
else if (Array.isArray(first) && Object.isFrozen(first)) {
first = templateValues(args).join("");
}
let { props, prop } = this.__element_info__;
props = { ...props, [prop]: first }
return elementBuilder({ props, prop: null });
}
function getPropsValues(props) {
let { props: existingProps } = this.__element_info__;
props = { ...existingProps, ...props }
return elementBuilder({ props, prop: null });
}
function elementBuilder(propsInfo) {
let builder = new Proxy(() => { }, {
apply(target, thisArg, children) {
let { props } = builder.__element_info__;
if (typeof props.exec === "function") {
let exec = props.exec;
delete props.exec;
let result = exec(builder, children);
props.exec = exec;
return result;
}
let [first] = children;
if (Array.isArray(first) && Object.isFrozen(first)) {
children = templateValues(children);
}
for (let i = 0; i < children.length; i++) {
let arg = children[i];
if (typeof arg === "function" && arg.__element_info__) {
children[i] = arg();
}
}
return elementConstructor(element, props, ...children);
},
get(target, prop) {
const result = target[prop];
if (typeof result !== "undefined") {
return result;
}
if (prop === "props") {
return getPropsValues;
}
else if (typeof prop === "string") {
if (prop.startsWith("data")) {
prop = prop.replace(/[A-Z]/g, m => "-" + m.toLowerCase())
}
// @ts-ignore
target.__element_info__.prop = prop;
return getPropertyValue;
}
},
set(target, prop, value) {
target[prop] = value;
return true;
}
})
builder.__element_info__ = propsInfo;
return builder;
}
return elementBuilder({ props: {}, prop: null });
}
function elementBuildersBuilder(elementConstructor = createElement, elements = []) {
if (Object.prototype.toString.call(elementConstructor) === '[object Object]') {
elementConstructor = elementConstructor["elementConstructor"] || createElement;
elements = elementConstructor["elements"] || [];
}
elementConstructor = elementConstructor || createElement;
if (elements.length > 0) {
let builders = [];
for (const element of elements) {
builders.push(elementBuilderBuilder(elementConstructor, element));
}
return builders;
}
else {
return new Proxy(() => { }, {
apply(target, thisArg, args) {
return elementBuildersBuilder(...args);
},
get(target, prop) {
const result = target[prop];
if (typeof result !== "undefined") {
return result;
}
target[prop] = elementBuilderBuilder(elementConstructor, prop);
return target[prop];
}
});
}
}
const elementBuilders = elementBuildersBuilder();
window.Webscript = { elementBuilders };
})();
instead of using the document rendering, I just returned simple objects. Also the window
is just a custom object instantiated beforehand in the execution environment. The following is a test script that returns the result in the screenshot.
(function() {
const {body, vstack, hstack, button, space} = window.Webscript.elementBuilders;
const app = body.style({background:'black'})(
vstack(
button.style({width:"100", height:"20"}).text`Hello`(),
space.padding`10`(),
button.style({width:"100",height:"20"}).text`World`()
),
hstack(
button.style({width:"100", height:"20"}).text`Hello`(),
space.padding`10`(),
button.style({width:"100",height:"20"}).text`World`()
)
);
console.log(app);
})();
That's great! One note, if you want to, you can remove the ()
at the end of the element builders.
For example this
space.padding`10`()
Could be this:
space.padding`10`
Because the element builder it is within (vstack) will check if it has been executed (converted to an element) and if it has not been, then it will execute it and use the returned value.
ok I added a special createElement
for handling validations. For this I have to export the default createElement
function. My question is if there another way to create custom object validation instead of overriding the createElement
function?. Thanks :)
(function() {
const createBody = (tagName, props, ...children) => {
if(props.style.background != "red") {
console.error(props);
throw new Error("not red");
}
return window.createElement(tagName, props, children);
};
const handlers = {
body: createBody
};
const createElement = (tagName, props, ...children) => {
if(handlers[tagName]) {
const handler = handlers[tagName];
return handler(tagName, props, children);
}
return window.createElement(tagName, props, children);
};
const { body, vstack, hstack, button, space} = window.Webscript.elementBuilders(createElement);
const app = body.style({background:'red'})(
vstack(
button.style({width:"100", height:"20"}).text(`Hello`),
space.padding`10`,
button.style({width:"100",height:"20"}).text`World`
),
hstack(
button.style({width:"100", height:"20"}).text`Hello`,
space.padding`10`,
button.style({width:"100",height:"20"}).text`World`
)
);
console.log(app);
})();
There isn't another way to do object validation. I think the way you are doing it is a good way.
By the way, I made the following change today. Let me know what you think about it:
I removed the default createElement
function from webscript.js.
Because Webscript is designed to be used with custom implementations of createElement
or be used with existing UI libraries that supply the createElement
function.
I put the default createElement
in a new file createelement.js
to serve as a default or starting implementation. People can use it how it is or modify it.
I think is good to separate concerns. But I also think a default alternative must be available. Similar to htm
https://github.com/developit/htm
// hotlinking from unpkg: (no build tool needed!)
import htm from 'https://unpkg.com/htm?module'
const html = htm.bind(React.createElement);
// just want htm + preact in a single file? there's a highly-optimized version of that:
import { html, render } from 'https://unpkg.com/htm/preact/standalone.module.js'
I think a "standalone" alternative that includes a default createElement
should be provided too :)
I will close this issue since the main question was addressed 👍
Yes, I agree. It is a good idea. Let's add that in.
Webscript now provides ES5 support.
Hello this seems like a nice way to structure an app. I currently would like to support iOS's Javascript Core. but the import mechanism is different in that environment (not supported). All other ES6 features function well though.
https://stackoverflow.com/questions/48354804/how-to-import-modules-in-swifts-javascriptcore
A solution is transpiling this lib to ES5 using babel with browserify or another bundle.
Is there a way to use this lib with an ES5
<script type="text/javascript" src="webscript.js">
tag? or at least have use separate functions that could be imported without theimport
keyword?.Thanks for this lib 👍