Merge pull request #726 from trheyi/main

Add s:render feature for data update rendering
This commit is contained in:
Max 2024-08-03 19:04:55 +08:00 committed by GitHub
commit d9769af569
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
21 changed files with 1103 additions and 375 deletions

View file

@ -130,7 +130,8 @@ bindata:
cp -r ui .tmp/data/public
cp -r xgen .tmp/data/
cp -r yao .tmp/data/
cp -r builder .tmp/data/
cp -r sui/libsui .tmp/data/
find .tmp/data -name ".DS_Store" -type f -delete
go-bindata -fs -pkg data -o data/bindata.go -prefix ".tmp/data/" .tmp/data/...
rm -rf .tmp/data
rm -rf .tmp/yao-init

View file

@ -1,3 +0,0 @@
function foo() {
return "foo";
}

View file

@ -1 +0,0 @@
<div>Yao Builder</div>

File diff suppressed because one or more lines are too long

View file

@ -46,18 +46,6 @@ func Setup() *assetfs.AssetFS {
panic("unreachable")
}
// Builder Builder ui
func Builder() *assetfs.AssetFS {
assetInfo := func(path string) (os.FileInfo, error) {
return os.Stat(path)
}
for k := range _bintree.Children {
k = "builder"
return &assetfs.AssetFS{Asset: Asset, AssetDir: AssetDir, AssetInfo: assetInfo, Prefix: k, Fallback: "index.html"}
}
panic("unreachable")
}
// ReplaceXGen bindata file
func ReplaceXGen(search, replace string) error {
err := replaceXGenIndex(search, replace)

View file

@ -18,9 +18,6 @@ var AppFileServer http.Handler
// XGenFileServerV1 XGen v1.0
var XGenFileServerV1 http.Handler = http.FileServer(data.XgenV1())
// BuilderFileServer Builder ui
var BuilderFileServer http.Handler = http.FileServer(data.Builder())
// AdminRoot cache
var AdminRoot = ""

View file

@ -10,6 +10,26 @@ var dsl = []byte(`
"guard": "bearer-jwt",
"group": "__yao/sui/v1",
"paths": [
{
"label": "Render",
"description": "Render the frontend page",
"path": "/render/*route",
"method": "POST",
"guard": "-",
"process": "sui.Render",
"in": [":context", "$param.route", ":payload"],
"out": { "status": 200, "type": "text/html; charset=utf-8" }
},
{
"label": "Run",
"description": "Run the backend script, with Api prefix method",
"path": "/run/*route",
"guard": "-",
"method": "POST",
"process": "sui.Run",
"in": [":context", "$param.route", ":payload"],
"out": { "status": 200, "type": "application/json" }
},
{
"path": "/:id/setting",
"method": "GET",

View file

@ -20,6 +20,9 @@ func init() {
process.RegisterGroup("sui", map[string]process.Handler{
"setting": Setting,
"render": Render,
"run": Run,
"template.get": TemplateGet,
"template.find": TemplateFind,
"template.asset": TemplateAsset,

111
sui/api/render.go Normal file
View file

@ -0,0 +1,111 @@
package api
import (
"fmt"
"github.com/gin-gonic/gin"
"github.com/yaoapp/gou/process"
"github.com/yaoapp/yao/sui/core"
)
// Render the frontend page
func Render(process *process.Process) interface{} {
process.ValidateArgNums(3)
ctx, ok := process.Args[0].(*gin.Context)
if !ok {
return "The context is required"
}
ctx.Header("Content-Type", "text/html; charset=utf-8")
route := process.ArgsString(1)
payload := process.ArgsMap(2)
if route == "" {
return "The route is required"
}
if payload["name"] == nil {
return "The render name is required"
}
ctx.Request.URL.Path = route
r, _, err := NewRequestContext(ctx)
if err != nil {
return fmt.Sprintf("<div class='text-danger'> %s </div>", err.Error())
}
var c *core.Cache = nil
if !r.Request.DisableCache() {
c = core.GetCache(r.File)
}
if c == nil {
c, _, err = r.MakeCache()
if err != nil {
return fmt.Sprintf("<div class='text-danger'> %s </div>", err.Error())
}
}
if c == nil {
return fmt.Sprintf("<div class='text-danger'> Cache not found </div>")
}
data, ok := payload["data"].(map[string]interface{})
if !ok {
return fmt.Sprintf("<div class='text-danger'> Data not found </div>")
}
name, ok := payload["name"].(string)
if !ok {
return fmt.Sprintf("<div class='text-danger'> Name not found </div>")
}
html, err := r.renderHTML(c, name, c.HTML, core.Data(data))
if err != nil {
return fmt.Sprintf("<div class='text-danger'> %s </div>", err.Error())
}
return html
}
func (r *Request) renderHTML(c *core.Cache, name string, html string, data core.Data) (string, error) {
doc, err := core.NewDocument([]byte(html))
if err != nil {
return "", fmt.Errorf("Document error: %w", err)
}
sel := doc.Find(fmt.Sprintf("[s\\:render='%s']", name))
if sel.Length() == 0 {
return "", fmt.Errorf("Render %s not found", name)
}
// Set the page request data
option := core.ParserOption{
Theme: r.Request.Theme,
Locale: r.Request.Locale,
Debug: r.Request.DebugMode(),
DisableCache: r.Request.DisableCache(),
Route: r.Request.URL.Path,
Root: c.Root,
Script: c.Script,
Imports: c.Imports,
Request: r.Request,
}
// Parse the template
parser := core.NewTemplateParser(data, &option)
err = parser.RenderSelection(sel)
if err != nil {
return "", fmt.Errorf("Parser error: %w", err)
}
sel.Find("[sui-hide]").Remove()
parser.Tidy(sel)
html, err = sel.Html()
if err != nil {
return "", fmt.Errorf("Html error: %w", err)
}
return html, nil
}

8
sui/api/run.go Normal file
View file

@ -0,0 +1,8 @@
package api
import "github.com/yaoapp/gou/process"
// Run the backend script, with Api prefix method
func Run(process *process.Process) interface{} {
return nil
}

View file

@ -71,6 +71,19 @@ func (page *Page) Build(ctx *BuildContext, option *BuildOption) (*goquery.Docume
ctx.warnings = append(ctx.warnings, warnings...)
}
// Prepend the libsui.min.js
if !option.IgnoreLibSUI {
script := ScriptNode{
Parent: "head",
Attrs: []html.Attribute{
{Key: "src", Val: fmt.Sprintf("%s/libsui.min.js", option.AssetRoot)},
{Key: "type", Val: "text/javascript"},
{Key: "name", Val: "libsui"},
},
}
ctx.scripts = append([]ScriptNode{script}, ctx.scripts...)
}
// Scripts
scripts, err := page.BuildScripts(ctx, option, "__page", namespace)
if err != nil {

View file

@ -58,13 +58,6 @@ func (page *Page) Compile(ctx *BuildContext, option *BuildOption) (string, []str
}
// SUI lib
lib, err := libsui(option.ScriptMinify)
if err != nil {
return "", warnings, err
}
head.AppendHtml("\n\n" + `<script name="sui" type="text/javascript">` + lib + `</script>` + "\n\n")
// Page Config
page.Config = page.GetConfig()

View file

@ -94,10 +94,12 @@ func GetEventScript(sequence int, sel *goquery.Selection, ns string, cn string,
for name, handler := range events {
if ispage {
source += pageEventInjectScript(id, name, dataRaw, jsonRaw, handler) + "\n"
sel.SetAttr("s:event-cn", "__page")
} else {
source += compEventInjectScript(id, name, cn, dataRaw, jsonRaw, handler) + "\n"
sel.SetAttr("s:event-cn", cn)
}
sel.RemoveAttr(fmt.Sprintf("s:on-%s", name))
// sel.RemoveAttr(fmt.Sprintf("s:on-%s", name))
}
sel.SetAttr("s:event", id)

View file

@ -5,226 +5,45 @@ import (
"github.com/evanw/esbuild/pkg/api"
"github.com/yaoapp/gou/runtime/transform"
"github.com/yaoapp/yao/data"
)
var libsuicode = ""
func libsui(minify bool) (string, error) {
if libsuicode != "" {
return libsuicode, nil
}
// LibSUI return the libsui code
func LibSUI() ([]byte, []byte, error) {
option := api.TransformOptions{Target: api.ES2015}
if minify {
option.MinifyIdentifiers = true
option.MinifySyntax = true
option.MinifyWhitespace = true
}
var err error
libsuicode, err = transform.JavaScript(libsuisource, option)
// Read source code from bindata
index, err := data.Read("libsui/index.ts")
if err != nil {
return "", fmt.Errorf("libsui error: %w", err)
return nil, nil, err
}
return libsuicode, nil
utils, err := data.Read("libsui/utils.ts")
if err != nil {
return nil, nil, err
}
yao, err := data.Read("libsui/yao.ts")
if err != nil {
return nil, nil, err
}
// Merge the source code
source := fmt.Sprintf("%s\n%s\n%s", index, utils, yao)
// Build the source code
js, sm, err := transform.TypeScriptWithSourceMap(string(source), api.TransformOptions{
Target: api.ES2015,
MinifyIdentifiers: true,
MinifySyntax: true,
MinifyWhitespace: true,
Sourcefile: "libsui.ts",
})
return js, sm, nil
}
const libsuisource = `
function $$(selector) {
elm = null;
if (typeof selector === "string" ){
elm = document.querySelector(selector);
}
if (selector instanceof HTMLElement) {
elm = selector;
}
if (elm) {
cn = elm.getAttribute("s:cn");
if (cn != "" && typeof window[cn] === "function") {
const component = new window[cn](elm);
return new __sui_component(elm, component);
}
}
return null;
}
const $utils = {
RemoveClass: (element, className) => {
const classes = Array.isArray(className) ? className : className.split(" ");
classes.forEach((c) => {
const v = c.replace(/[\n\r\s]/g, "");
if (v === "") return;
element.classList.remove(v);
});
},
AddClass: (element, className) => {
const classes = Array.isArray(className) ? className : className.split(" ");
classes.forEach((c) => {
const v = c.replace(/[\n\r\s]/g, "");
if (v === "") return;
element.classList.add(v);
});
},
}
function __sui_component_root(elm, name) {
while (elm && elm.getAttribute("s:cn") !== name) {
elm = elm.parentElement;
}
return elm;
}
function __sui_state(component) {
this.handlers = component.watch || {};
this.Set = async function (key, value, target) {
const handler = this.handlers[key];
target = target || component.root;
if (handler && typeof handler === "function") {
const stateObj = {
target: target,
stopPropagation: function () {
target.setAttribute("state-propagation", "true");
},
}
await handler(value, stateObj);
const isStopPropagation = target ? target.getAttribute("state-propagation") === "true" : false;
if (isStopPropagation) {
return;
}
let parent = component.root.parentElement;
while (parent && !parent.getAttribute("s:cn")) {
parent = parent.parentElement;
}
if ( parent == document.body || parent == null) {
return;
}
// Dispatch the state change custom event to parent component
const event = new CustomEvent("state:change", { detail: { key: key, value: value, target:component.root } });
parent.dispatchEvent(event);
}
}
}
function __sui_props(elm) {
this.Get = function (key) {
if (!elm || typeof elm.getAttribute !== "function") {
return null;
}
const k = "prop:" + key;
const v = elm.getAttribute(k);
const json = elm.getAttribute("json-attr-prop:" + key) === "true";
if (json) {
try {
return JSON.parse(v);
} catch (e) {
return null;
}
}
return v;
}
this.List = function () {
const props = {};
if (!elm || typeof elm.getAttribute !== "function") {
return props;
}
const attrs = elm.attributes;
for (let i = 0; i < attrs.length; i++) {
const attr = attrs[i];
if (attr.name.startsWith("prop:")) {
const k = attr.name.replace("prop:", "");
const json = elm.getAttribute("json-attr-prop:" + k) === "true";
if (json) {
try {
props[k] = JSON.parse(attr.value);
} catch (e) {
props[k] = null;
}
continue;
}
props[k] = attr.value;
}
}
return props;
}
}
function __sui_component(elm, component) {
this.root = elm;
this.store = new __sui_store(elm);
this.props = new __sui_props(elm);
this.state = component ? new __sui_state(component) : {};
}
function __sui_event_handler(event, dataKeys, jsonKeys, target, root, handler) {
const data = {};
target = target || null;
if (target) {
dataKeys.forEach(function (key) {
const value = target.getAttribute("data:" + key);
data[key] = value;
})
jsonKeys.forEach(function (key) {
const value = target.getAttribute("json:" + key);
data[key] = null;
if (value && value != "") {
try {
data[key] = JSON.parse(value);
} catch (e) {
const message = e.message || e || "An error occurred";
console.error(` + "`[SUI] Event Handler Error: ${message}`" + `, target);
}
}
})
}
handler && handler(event, data, {
rootElement: root,
targetElement: target
});
};
function __sui_store(elm) {
elm = elm || document.body;
this.Get = function (key) {
return elm.getAttribute("data:" + key);
}
this.Set = function (key, value) {
elm.setAttribute("data:" + key, value);
}
this.GetJSON = function (key) {
const value = elm.getAttribute("json:" + key);
if (value && value != "") {
try {
const res = JSON.parse(value);
return res;
} catch (e) {
const message = e.message || e || "An error occurred";
console.error(` + "`[SUI] Event Handler Error: ${message}`" + `, elm);
return null;
}
}
return null;
}
this.SetJSON = function (key, value) {
elm.setAttribute("json:" + key, JSON.stringify(value));
}
this.GetData = function () {
return this.GetJSON("__component_data") || {};
}
}
`
const initScriptTmpl = `
try {
var __sui_data = %s;
@ -237,7 +56,7 @@ const initScriptTmpl = `
const cn = element.getAttribute("s:cn");
if (method && typeof window[cn] === "function") {
try {
window[cn](element);
new window[cn](element);
} catch (e) {
const message = e.message || e || "An error occurred";
console.error(` + "`[SUI] ${cn} Error: ${message}`" + `);
@ -264,24 +83,30 @@ const i118nScriptTmpl = `
`
const pageEventScriptTmpl = `
document.querySelector("[s\\:event=%s]").addEventListener("%s", function (event) {
const dataKeys = %s;
const jsonKeys = %s;
const root = document.body;
const target = this;
__sui_event_handler(event, dataKeys, jsonKeys, target, root, %s);
});
if (document.querySelector("[s\\:event=%s]")) {
let elms = document.querySelectorAll("[s\\:event=%s]");
elms.forEach(function (element) {
element.addEventListener("%s", function (event) {
const dataKeys = %s;
const jsonKeys = %s;
const root = document.body;
__sui_event_handler(event, dataKeys, jsonKeys, element, root, window.%s);
});
});
}
`
const compEventScriptTmpl = `
if (document.querySelector("[s\\:event=%s]")) {
document.querySelector("[s\\:event=%s]").addEventListener("%s", function (event) {
const dataKeys = %s;
const jsonKeys = %s;
const root = __sui_component_root(this, "%s");
handler = new %s(root).%s;
const target = event.target || null;
__sui_event_handler(event, dataKeys, jsonKeys, target, root, handler);
let elms = document.querySelectorAll("[s\\:event=%s]");
elms.forEach(function (element) {
element.addEventListener("%s", function (event) {
const dataKeys = %s;
const jsonKeys = %s;
const root = __sui_component_root(element, "%s");
handler = new %s(root).%s;
__sui_event_handler(event, dataKeys, jsonKeys, element, root, handler);
});
});
}
`
@ -333,7 +158,7 @@ func headInjectionScript(jsonRaw string) string {
}
func pageEventInjectScript(eventID, eventName, dataKeys, jsonKeys, handler string) string {
return fmt.Sprintf(pageEventScriptTmpl, eventID, eventName, dataKeys, jsonKeys, handler)
return fmt.Sprintf(pageEventScriptTmpl, eventID, eventID, eventName, dataKeys, jsonKeys, handler)
}
func compEventInjectScript(eventID, eventName, component, dataKeys, jsonKeys, handler string) string {

View file

@ -68,17 +68,21 @@ var keepWords = map[string]bool{
}
var allowUsePropAttrs = map[string]bool{
"s:if": true,
"s:elif": true,
"s:for": true,
"s:event": true,
"s:if": true,
"s:elif": true,
"s:for": true,
"s:event": true,
"s:event-cn": true,
"s:render": true,
}
var keepAttrs = map[string]bool{
"s:ns": true,
"s:cn": true,
"s:ready": true,
"s:event": true,
"s:ns": true,
"s:cn": true,
"s:ready": true,
"s:event": true,
"s:event-cn": true,
"s:render": true,
}
// NewTemplateParser create a new template parser
@ -157,7 +161,7 @@ func (parser *TemplateParser) Render(html string) (string, error) {
if parser.option != nil && (parser.option.Request != nil || parser.option.Preview) {
// Remove the sui-hide attribute
doc.Find("[sui-hide]").Remove()
parser.tidy(doc.Selection)
parser.Tidy(doc.Selection)
}
// fmt.Println(doc.Html())
@ -837,13 +841,13 @@ func (parser *TemplateParser) show(sel *goquery.Selection) {
// sel.SetAttr("style", style)
}
func (parser *TemplateParser) tidy(s *goquery.Selection) {
func (parser *TemplateParser) Tidy(s *goquery.Selection) {
s.Contents().Each(func(i int, child *goquery.Selection) {
node := child.Get(0)
if _, exist := child.Attr("s:jit"); node.Data == "slot" || exist {
parser.tidy(child)
parser.Tidy(child)
parser.removeWrapper(child)
return
}
@ -861,7 +865,7 @@ func (parser *TemplateParser) tidy(s *goquery.Selection) {
// Remove the parsed attribute
attrs := []html.Attribute{}
for _, attr := range node.Attr {
if strings.HasPrefix(attr.Key, "s:") && !keepAttrs[attr.Key] {
if strings.HasPrefix(attr.Key, "s:") && !keepAttrs[attr.Key] && !strings.HasPrefix(attr.Key, "s:on-") {
continue
}
@ -872,7 +876,7 @@ func (parser *TemplateParser) tidy(s *goquery.Selection) {
}
node.Attr = attrs
parser.tidy(child)
parser.Tidy(child)
})
}

View file

@ -262,6 +262,7 @@ type BuildOption struct {
PublicRoot string `json:"public_root,omitempty"`
AssetRoot string `json:"asset_root,omitempty"`
IgnoreAssetRoot bool `json:"ignore_asset_root,omitempty"`
IgnoreLibSUI bool `json:"ignore_lib_sui,omitempty"`
IgnoreDocument bool `json:"ignore_document,omitempty"`
JitMode bool `json:"jit_mode,omitempty"`
WithWrapper bool `json:"with_wrapper,omitempty"`

340
sui/libsui/index.ts Normal file
View file

@ -0,0 +1,340 @@
function $$(selector) {
let elm: HTMLElement | null = null;
if (typeof selector === "string") {
elm = document.querySelector(selector);
}
if (selector instanceof HTMLElement) {
elm = selector;
}
if (elm) {
const cn = elm.getAttribute("s:cn");
if (cn && cn != "" && typeof window[cn] === "function") {
const component = new window[cn](elm);
return new __sui_component(elm, component);
}
}
return null;
}
function __sui_component_root(elm: Element, name: string) {
return elm.closest(`[s\\:cn=${name}]`);
}
function __sui_state(component) {
this.handlers = component.watch || {};
this.Set = async function (key, value, target) {
const handler = this.handlers[key];
target = target || component.root;
if (handler && typeof handler === "function") {
const stateObj = {
target: target,
stopPropagation: function () {
target.setAttribute("state-propagation", "true");
},
};
await handler(value, stateObj);
const isStopPropagation = target
? target.getAttribute("state-propagation") === "true"
: false;
if (isStopPropagation) {
return;
}
let parent = component.root.parentElement;
while (parent && !parent.getAttribute("s:cn")) {
parent = parent.parentElement;
}
if (parent == document.body || parent == null) {
return;
}
// Dispatch the state change custom event to parent component
const event = new CustomEvent("state:change", {
detail: { key: key, value: value, target: component.root },
});
parent.dispatchEvent(event);
}
};
}
function __sui_props(elm) {
this.Get = function (key) {
if (!elm || typeof elm.getAttribute !== "function") {
return null;
}
const k = "prop:" + key;
const v = elm.getAttribute(k);
const json = elm.getAttribute("json-attr-prop:" + key) === "true";
if (json) {
try {
return JSON.parse(v);
} catch (e) {
return null;
}
}
return v;
};
this.List = function () {
const props = {};
if (!elm || typeof elm.getAttribute !== "function") {
return props;
}
const attrs = elm.attributes;
for (let i = 0; i < attrs.length; i++) {
const attr = attrs[i];
if (attr.name.startsWith("prop:")) {
const k = attr.name.replace("prop:", "");
const json = elm.getAttribute("json-attr-prop:" + k) === "true";
if (json) {
try {
props[k] = JSON.parse(attr.value);
} catch (e) {
props[k] = null;
}
continue;
}
props[k] = attr.value;
}
}
return props;
};
}
function __sui_component(elm, component) {
this.root = elm;
this.store = new __sui_store(elm);
this.props = new __sui_props(elm);
this.state = component ? new __sui_state(component) : {};
}
function __sui_event_handler(event, dataKeys, jsonKeys, target, root, handler) {
const data = {};
target = target || null;
if (target) {
dataKeys.forEach(function (key) {
const value = target.getAttribute("data:" + key);
data[key] = value;
});
jsonKeys.forEach(function (key) {
const value = target.getAttribute("json:" + key);
data[key] = null;
if (value && value != "") {
try {
data[key] = JSON.parse(value);
} catch (e) {
const message = e.message || e || "An error occurred";
console.error(`[SUI] Event Handler Error: ${message} `, target);
}
}
});
}
handler &&
handler(event, data, {
rootElement: root,
targetElement: target,
});
}
function __sui_event_init(elm: Element) {
const eventElms = elm.querySelectorAll("[s\\:event]");
eventElms.forEach((eventElm) => {
const cn = eventElm.getAttribute("s:event-cn") || "";
// Data keys
const events: Record<string, string> = {};
const dataKeys: string[] = [];
const jsonKeys: string[] = [];
for (let i = 0; i < eventElm.attributes.length; i++) {
if (eventElm.attributes[i].name.startsWith("data:")) {
dataKeys.push(eventElm.attributes[i].name.replace("data:", ""));
}
if (eventElm.attributes[i].name.startsWith("json:")) {
jsonKeys.push(eventElm.attributes[i].name.replace("json:", ""));
}
if (eventElm.attributes[i].name.startsWith("s:on-")) {
const key = eventElm.attributes[i].name.replace("s:on-", "");
events[key] = eventElm.attributes[i].value;
}
}
// Bind the event
for (const name in events) {
const bind = events[name];
if (cn == "__page") {
const handler = window[bind];
const root = document.body;
const target = eventElm;
eventElm.addEventListener(name, (event) => {
__sui_event_handler(event, dataKeys, jsonKeys, target, root, handler);
});
continue;
}
const comp = new window[cn](eventElm.closest(`[s\\:cn=${cn}]`));
const handler = comp[bind];
const root = comp.root;
const target = eventElm;
eventElm.addEventListener(name, (event) => {
__sui_event_handler(event, dataKeys, jsonKeys, target, root, handler);
});
}
});
}
function __sui_store(elm) {
elm = elm || document.body;
this.Get = function (key) {
return elm.getAttribute("data:" + key);
};
this.Set = function (key, value) {
elm.setAttribute("data:" + key, value);
};
this.GetJSON = function (key) {
const value = elm.getAttribute("json:" + key);
if (value && value != "") {
try {
const res = JSON.parse(value);
return res;
} catch (e) {
const message = e.message || e || "An error occurred";
console.error(`[SUI] Event Handler Error: ${message}`, elm);
return null;
}
}
return null;
};
this.SetJSON = function (key, value) {
elm.setAttribute("json:" + key, JSON.stringify(value));
};
this.GetData = function () {
return this.GetJSON("__component_data") || {};
};
}
/**
* SUI Render
* @param component
* @param name
*/
async function __sui_render(
component: Component | string,
name: string,
data: Record<string, any>,
option?: RenderOption
): Promise<string> {
const comp = (
typeof component === "object" ? component : $$(component)
) as Component;
if (comp == null) {
console.error(`[SUI] Component not found: ${component}`);
return Promise.reject("Component not found");
}
const elms = comp.root.querySelectorAll(`[s\\:render=${name}]`);
if (!elms.length) {
console.error(`[SUI] No element found with s:render=${name}`);
return Promise.reject("No element found");
}
// Set default options
option = option || {};
option.replace = option.replace === undefined ? true : option.replace;
option.showLoader =
option.showLoader === undefined ? false : option.showLoader;
option.withPageData =
option.withPageData === undefined ? false : option.withPageData;
// Prepare loader
let loader = `<span class="sui-render-loading">Loading...</span>`;
if (option.showLoader && option.replace) {
if (typeof option.showLoader === "string") {
loader = option.showLoader;
} else if (option.showLoader instanceof HTMLElement) {
loader = option.showLoader.outerHTML;
}
elms.forEach((elm) => (elm.innerHTML = loader));
}
// Prepare data
let _data = comp.store.GetData() || {};
if (option.withPageData) {
// @ts-ignore
_data = { ..._data, ...__sui_data };
}
const route = window.location.pathname;
const url = `/api/__yao/sui/v1/render${route}`;
const payload = { name, data: { ..._data, ...data }, option };
const headers = {
"Content-Type": "application/json",
Cookie: document.cookie,
};
// Native post request to the server
try {
const body = JSON.stringify(payload);
const response = await fetch(url, { method: "POST", headers, body: body });
const text = await response.text();
if (!option.replace) {
return Promise.resolve(text);
}
// Set the response text to the elements
elms.forEach((elm) => {
elm.innerHTML = text;
__sui_event_init(elm);
});
return Promise.resolve(text);
} catch (e) {
//Set the error message
elms.forEach((elm) => {
elm.innerHTML = `<span class="sui-render-error">Failed to render</span>`;
console.error("Failed to render", e);
});
return Promise.reject("Failed to render");
}
}
export type Component = {
root: HTMLElement;
state: ComponentState;
store: ComponentStore;
watch?: Record<string, (value: any, state?: State) => void>;
Constants?: Record<string, any>;
[key: string]: any;
};
export type RenderOption = {
target?: HTMLElement; // default is same with s:render target
showLoader?: HTMLElement | string | boolean; // default is false
replace?: boolean; // default is true
withPageData?: boolean; // default is false
};
export type ComponentState = {
Set: (key: string, value: any) => void;
};
export type ComponentStore = {
Get: (key: string) => string;
Set: (key: string, value: any) => void;
GetJSON: (key: string) => any;
SetJSON: (key: string, value: any) => void;
GetData: () => Record<string, any>;
};
export type State = {
target: HTMLElement;
stopPropagation();
};

177
sui/libsui/utils.ts Normal file
View file

@ -0,0 +1,177 @@
function $Store(elm) {
if (!elm) {
return null;
}
if (typeof elm === "string") {
elm = document.querySelectorAll(elm);
if (elm.length == 0) {
return null;
}
elm = elm[0];
}
// @ts-ignore
return new __sui_store(elm);
}
function $Query(selector: string): __Query {
return new __Query(selector);
}
class __Query {
selector: string | Element = "";
elements: NodeListOf<Element> | null = null;
element: Element | null = null;
constructor(selector: string | Element) {
if (typeof selector === "string") {
this.selector = selector;
this.elements = document.querySelectorAll(selector);
if (this.elements.length > 0) {
this.element = this.elements[0];
}
} else {
this.element = selector;
}
this.selector = selector;
}
elm(): Element | null {
return this.element;
}
elms(): NodeListOf<Element> | null {
return this.elements;
}
$$() {
if (!this.element) {
return null;
}
const root = this.element.closest("[s\\:cn]");
if (!root) {
return null;
}
// @ts-ignore
return $$(root);
}
each(callback: (element: __Query, index: number) => void) {
if (!this.elements) {
return;
}
this.elements.forEach((element, index) => {
callback(new __Query(element), index);
});
return;
}
store() {
if (!this.element || typeof this.element.getAttribute !== "function") {
return null;
}
// @ts-ignore
return new __sui_store(this.element);
}
attr(key) {
if (!this.element || typeof this.element.getAttribute !== "function") {
return null;
}
return this.element.getAttribute(key);
}
data(key) {
if (!this.element || typeof this.element.getAttribute !== "function") {
return null;
}
return this.element.getAttribute("data:" + key);
}
json(key) {
if (!this.element || typeof this.element.getAttribute !== "function") {
return null;
}
const v = this.element.getAttribute("json:" + key);
if (!v) {
return null;
}
try {
return JSON.parse(v);
} catch (e) {
console.error(`Error parsing JSON for key ${key}: ${e}`);
return null;
}
}
hasClass(className) {
return this.element?.classList.contains(className);
}
prop(key) {
if (!this.element || typeof this.element.getAttribute !== "function") {
return null;
}
const k = "prop:" + key;
const v = this.element.getAttribute(k);
const json = this.element.getAttribute("json-attr-prop:" + key) === "true";
if (json && v) {
try {
return JSON.parse(v);
} catch (e) {
console.error(`Error parsing JSON for prop ${key}: ${e}`);
return null;
}
}
return v;
}
removeClass(className) {
const classes = Array.isArray(className) ? className : className.split(" ");
classes.forEach((c) => {
const v = c.replace(/[\n\r\s]/g, "");
if (v === "") return;
this.element?.classList.remove(v);
});
return this;
}
addClass(className) {
const classes = Array.isArray(className) ? className : className.split(" ");
classes.forEach((c) => {
const v = c.replace(/[\n\r\s]/g, "");
if (v === "") return;
this.element?.classList.add(v);
});
return this;
}
html(html?: string): __Query | string {
if (html === undefined) {
return this.element?.innerHTML || "";
}
if (this.element) {
this.element.innerHTML = html;
}
return this;
}
}
function $Render(comp, option): __Render {
const r = new __Render(comp, option);
return r;
}
class __Render {
comp = null;
option = null;
constructor(comp, option) {
this.comp = comp;
this.option = option;
}
async Render(name, data): Promise<string> {
// @ts-ignore
return __sui_render(this.comp, name, data, this.option);
}
}

182
sui/libsui/yao.ts Normal file
View file

@ -0,0 +1,182 @@
/**
* YAO Pure JavaScript SDK
* @author Max<max@iqka.com>
* @maintainer https://yaoapps.com
*/
/**
* Yao Object
* @param {*} host
*/
function Yao(host) {
this.host = `${
host || window.location.protocol + "//" + window.location.host
}/api`;
this.query = {};
new URLSearchParams(window.location.search).forEach((key, value) => {
this.query[key] = value;
});
}
/**
* Get API
* @param {*} path
* @param {*} params
*/
Yao.prototype.Get = async function (path, params, headers) {
return this.Fetch("GET", path, params, null, headers);
};
/**
* Post API
* @param {*} path
* @param {*} data
* @param {*} params
* @param {*} headers
*/
Yao.prototype.Post = async function (path, data, params, headers) {
return this.Fetch("POST", path, params, data, headers);
};
/**
* Download API
* @param {*} path
* @param {*} params
*/
Yao.prototype.Download = async function (path, params, savefile, headers) {
try {
const blob = await this.Fetch("GET", path, params, null, headers, true);
var objectUrl = window.URL.createObjectURL(blob);
let anchor = document.createElement("a");
document.body.appendChild(anchor);
anchor.href = objectUrl;
anchor.download = savefile;
anchor.click();
window.URL.revokeObjectURL(objectUrl);
} catch (err) {
alert("成功创建导出任务!");
}
};
/**
* Fetch API
* @param {*} method
* @param {*} path
* @param {*} params
* @param {*} data
* @param {*} headers
*/
Yao.prototype.Fetch = async function (
method,
path,
params,
data,
headers,
isblob
) {
params = params || {};
headers = headers || {};
data = data || null;
var url = `${this.host}${path}`;
var queryString = this.Serialize(params);
if (queryString != "") {
url = url.includes("?") ? `${url}&${queryString}` : `${url}?${queryString}`;
}
const token = this.Token();
if (token != "") {
headers["authorization"] = `Bearer ${token}`;
}
if (!headers["Content-Type"]) {
headers["Content-Type"] = "application/json";
}
var options: any = {
method: method,
mode: "cors", // no-cors, *cors, same-origin
cache: "no-cache", // *default, no-cache, reload, force-cache, only-if-cached
credentials: "same-origin", // include, *same-origin, omit
headers: headers,
redirect: "follow", // manual, *follow, error
};
if (data != null) {
options["body"] = JSON.stringify(data);
}
const resp = await fetch(url, options);
const type = resp.headers.get("Content-Type") || "";
if (type.includes("application/json")) {
try {
const data = await resp.json();
return data;
} catch (err) {
return { code: resp.status, message: "empty return" };
}
} else if (isblob) {
return resp.blob();
} else if (type.includes("text/html") || type.includes("text/plain")) {
return resp.text();
}
return resp.text();
};
/**
* Token API
* @param {*} path
* @param {*} params
*/
Yao.prototype.Token = function () {
var token = sessionStorage.getItem("token") || "";
if (token == "") {
return this.Cookie("__tk") || "";
}
return token;
};
/**
* Get Cookie
* @param {*} cookieName
* @returns
*/
Yao.prototype.Cookie = function (cookieName) {
var name = cookieName + "=";
var decodedCookie = decodeURIComponent(document.cookie);
var cookieArray = decodedCookie.split(";");
for (var i = 0; i < cookieArray.length; i++) {
var cookie = cookieArray[i].trim();
if (cookie.indexOf(name) === 0) {
return cookie.substring(name.length, cookie.length);
}
}
return null;
};
Yao.prototype.SetCookie = function (cookieName, cookieValue, expireDays) {
expireDays = expireDays || 30;
var d = new Date();
d.setTime(d.getTime() + expireDays * 24 * 60 * 60 * 1000);
var expires = "expires=" + d.toUTCString();
document.cookie = `${cookieName}=${cookieValue};${expires};path=/`;
};
Yao.prototype.DeleteCookie = function (cookieName) {
document.cookie = `${cookieName}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;`;
};
/**
* Serialize To Query String
* @param {*} obj
* @returns
*/
Yao.prototype.Serialize = function (obj) {
const str: string[] = [];
for (const p in obj)
if (obj.hasOwnProperty(p)) {
str.push(encodeURIComponent(p) + "=" + encodeURIComponent(obj[p]));
}
return str.join("&");
};

View file

@ -124,6 +124,12 @@ func (tmpl *Template) Build(option *core.BuildOption) ([]string, error) {
return warnings, err
}
// Add sui lib to the global
err = tmpl.UpdateJSSDK(option)
if err != nil {
return warnings, err
}
// Execute the build after hook
if option.ExecScripts {
res := tmpl.ExecAfterBuildScripts()
@ -200,6 +206,44 @@ func (tmpl *Template) SyncAssetFile(file string, option *core.BuildOption) error
return copy(sourceFile, targetFile)
}
// UpdateJSSDK update the js sdk
func (tmpl *Template) UpdateJSSDK(option *core.BuildOption) error {
jsCode, sourceMap, err := core.LibSUI()
if err != nil {
return err
}
// get source abs path
root, err := tmpl.local.DSL.PublicRoot(option.Data)
if err != nil {
log.Error("SyncAssets: Get the public root error: %s. use %s", err.Error(), tmpl.local.DSL.Public.Root)
root = tmpl.local.DSL.Public.Root
}
targetRoot := filepath.Join(application.App.Root(), "public", root, "assets")
file := filepath.Join(targetRoot, "libsui.min.js")
mapFile := filepath.Join(targetRoot, "libsui.min.js.map")
// create the target directory
if exist, _ := os.Stat(targetRoot); exist == nil {
os.MkdirAll(targetRoot, os.ModePerm)
}
// write the js sdk
// add source map url
jsCode = append(jsCode, []byte("\n//# sourceMappingURL=libsui.min.js.map")...)
err = os.WriteFile(file, jsCode, 0644)
if err != nil {
return err
}
// write the source map
err = os.WriteFile(mapFile, sourceMap, 0644)
return nil
}
func (tmpl *Template) writeGlobalScript(data map[string]interface{}) error {
file, source, err := tmpl.backendScriptSource("__global.backend")
if err != nil {

View file

@ -28,10 +28,10 @@ func TestPageEditorRender(t *testing.T) {
assert.NotEmpty(t, res.HTML)
assert.NotEmpty(t, res.CSS)
assert.NotEmpty(t, res.Scripts)
// assert.NotEmpty(t, res.Scripts)
assert.NotEmpty(t, res.Styles)
assert.GreaterOrEqual(t, len(res.Styles), 1)
assert.GreaterOrEqual(t, len(res.Scripts), 1)
// assert.GreaterOrEqual(t, len(res.Scripts), 1)
}