mudgen / webscript

Webscript is a Javascript library for creating DOM elements. Use it to create web applications. It is like HTML but it is Javascript. It is designed to work with existing libraries.
https://mudgen.github.io/webscript/docs/
MIT License
87 stars 9 forks source link

ES5 support? #2

Closed clsource closed 4 years ago

clsource commented 4 years ago

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 the import keyword?.

Thanks for this lib 👍

mudgen commented 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?

clsource commented 4 years ago

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 👍

mudgen commented 4 years ago

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.

clsource commented 4 years ago

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);

 })();

imagen

mudgen commented 4 years ago

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.

clsource commented 4 years ago

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);

 })();
mudgen commented 4 years ago

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.

clsource commented 4 years ago

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 👍

mudgen commented 4 years ago

Yes, I agree. It is a good idea. Let's add that in.

mudgen commented 4 years ago

Webscript now provides ES5 support.