From 0b54ba0909840914eb0ae8c14ccae87fc36e9871 Mon Sep 17 00:00:00 2001 From: sagar davara Date: Fri, 7 Feb 2025 20:42:57 +0530 Subject: [PATCH 1/9] fix: add global or import scripts into DOM --- .../src/ensemble/screens/help.yaml | 8 ++++++ packages/framework/src/evaluate/evaluate.ts | 25 +---------------- packages/runtime/src/runtime/screen.tsx | 27 +++++++++++++++++++ 3 files changed, 36 insertions(+), 24 deletions(-) diff --git a/apps/kitchen-sink/src/ensemble/screens/help.yaml b/apps/kitchen-sink/src/ensemble/screens/help.yaml index e90cb49a7..6b28b4012 100644 --- a/apps/kitchen-sink/src/ensemble/screens/help.yaml +++ b/apps/kitchen-sink/src/ensemble/screens/help.yaml @@ -32,6 +32,14 @@ View: styles: names: heading-1 text: Help + - Button: + styles: + className: page-1 ${ensemble.storage.get('zzz') + '-' + ensemble.storage.get('aaa')} + label: ${productTitleName} + onTap: + executeCode: | + // Calls a function defined in test.js + sayHello(); - Markdown: text: More to come! In the meantime, checkout the Ensemble [documentation](https://docs.ensembleui.com/). - Card: diff --git a/packages/framework/src/evaluate/evaluate.ts b/packages/framework/src/evaluate/evaluate.ts index f10ec7b15..6e0be94f0 100644 --- a/packages/framework/src/evaluate/evaluate.ts +++ b/packages/framework/src/evaluate/evaluate.ts @@ -38,14 +38,9 @@ export const buildEvaluateFn = ( // Need to filter out invalid JS identifiers ].filter(([key, _]) => !key.includes(".")), ); - const globalBlock = screen.model?.global; - const importedScriptBlock = screen.model?.importedScripts; // eslint-disable-next-line @typescript-eslint/no-implied-eval, no-new-func - const jsFunc = new Function( - ...Object.keys(invokableObj), - addScriptBlock(formatJs(js), globalBlock, importedScriptBlock), - ); + const jsFunc = new Function(...Object.keys(invokableObj), formatJs(js)); // eslint-disable-next-line @typescript-eslint/no-unsafe-return return () => jsFunc(...Object.values(invokableObj)); @@ -80,24 +75,6 @@ const formatJs = (js?: string): string => { return `return ${sanitizedJs}`; }; -const addScriptBlock = ( - js: string, - globalBlock?: string, - importedScriptBlock?: string, -): string => { - let jsString = ``; - - if (importedScriptBlock) { - jsString += `${importedScriptBlock}\n\n`; - } - - if (globalBlock) { - jsString += `${globalBlock}\n\n`; - } - - return (jsString += `${js}`); -}; - /** * @deprecated Consider using useEvaluate or createBinding which will * optimize creating the evaluation context diff --git a/packages/runtime/src/runtime/screen.tsx b/packages/runtime/src/runtime/screen.tsx index e0b329f16..2018e546d 100644 --- a/packages/runtime/src/runtime/screen.tsx +++ b/packages/runtime/src/runtime/screen.tsx @@ -91,6 +91,33 @@ export const EnsembleScreen: React.FC = ({ }; }, [screen.customWidgets]); + useEffect(() => { + const globalBlock = screen.global; + const importedScripts = screen.importedScripts; + + if (!globalBlock && !importedScripts) { + return; + } + + const jsString = ` + ${importedScripts || ""} + ${globalBlock || ""} + `; + + const script = document.createElement("script"); + script.id = screen.id; + script.type = "text/javascript"; + script.textContent = jsString; + + document.body.appendChild(script); + + console.log("<<<<<", window.global); + + return () => { + document.getElementById(screen.id)?.remove(); + }; + }, [screen.global, screen.importedScripts]); + if (!isInitialized) { return null; } From 903d02214164285c2af5d3f16b0cfa357d60b67b Mon Sep 17 00:00:00 2001 From: sagar davara Date: Mon, 10 Feb 2025 20:06:47 +0530 Subject: [PATCH 2/9] fix: implemented basic scope --- .../kitchen-sink/src/ensemble/scripts/common.js | 8 ++++---- packages/runtime/src/runtime/screen.tsx | 17 +++++++---------- 2 files changed, 11 insertions(+), 14 deletions(-) diff --git a/apps/kitchen-sink/src/ensemble/scripts/common.js b/apps/kitchen-sink/src/ensemble/scripts/common.js index e4eb6f875..eb031c965 100644 --- a/apps/kitchen-sink/src/ensemble/scripts/common.js +++ b/apps/kitchen-sink/src/ensemble/scripts/common.js @@ -1,5 +1,5 @@ -const productTitleName = "SgrDvr"; +productTitleName = "SgrDvr"; -const getDateLabel = (val) => { - return `i am a date label ${val}` -} \ No newline at end of file +getDateLabel = (val) => { + return `i am a date label ${val}`; +}; diff --git a/packages/runtime/src/runtime/screen.tsx b/packages/runtime/src/runtime/screen.tsx index 2018e546d..123819baf 100644 --- a/packages/runtime/src/runtime/screen.tsx +++ b/packages/runtime/src/runtime/screen.tsx @@ -99,22 +99,19 @@ export const EnsembleScreen: React.FC = ({ return; } - const jsString = ` - ${importedScripts || ""} - ${globalBlock || ""} - `; - const script = document.createElement("script"); - script.id = screen.id; script.type = "text/javascript"; - script.textContent = jsString; + script.textContent = `(function () { + ${importedScripts || ""} + ${globalBlock || ""} + }).call(this)`; document.body.appendChild(script); - console.log("<<<<<", window.global); - return () => { - document.getElementById(screen.id)?.remove(); + if (script.parentNode) { + script.parentNode.removeChild(script); + } }; }, [screen.global, screen.importedScripts]); From 1fd32ef30006b138bde5a48c3009b8df322751cc Mon Sep 17 00:00:00 2001 From: sagar davara Date: Wed, 12 Feb 2025 13:28:21 +0530 Subject: [PATCH 3/9] wip: script scope and arguments --- .../src/ensemble/screens/home.yaml | 509 +----------------- .../src/ensemble/scripts/common.js | 11 +- packages/framework/src/evaluate/evaluate.ts | 57 +- packages/runtime/src/runtime/screen.tsx | 34 +- 4 files changed, 87 insertions(+), 524 deletions(-) diff --git a/apps/kitchen-sink/src/ensemble/screens/home.yaml b/apps/kitchen-sink/src/ensemble/screens/home.yaml index f0d4a9c8a..408de1ec7 100644 --- a/apps/kitchen-sink/src/ensemble/screens/home.yaml +++ b/apps/kitchen-sink/src/ensemble/screens/home.yaml @@ -1,520 +1,21 @@ View: onLoad: - executeCode: - body: |- - ensemble.storage.set('iconName', 'home') - ensemble.storage.set('btnLoader' , false) - ensemble.storage.set('yyy' , 'page') - ensemble.storage.set('zzz' , 'button') - ensemble.storage.set('aaa' , '1') - ensemble.storage.set('bbb' , "0xffb91c1c") - ensemble.storage.set('loading', true); - console.log('>>> secret variable >>>', ensemble.secrets.dummyOauthSecret) - ensemble.storage.set('products', []); - ensemble.invokeAPI('getDummyProducts').then((res) => ensemble.storage.set('products', (res?.body?.users || []).map((i) => ({ ...i, name: i.firstName + ' ' + i.lastName })))); - const res = await ensemble.invokeAPI('getDummyNumbers') - await new Promise((resolve) => setTimeout(resolve, 5000)) - return res - onComplete: - executeCode: | - console.log('API triggered', result) - - header: - title: - Header: - - styles: - className: topView - scrollableView: true - backgroundColor: ${colors.primary['200']} + executeCode: | + ensemble.storage.set('productTitleName', 'test 123'); body: Column: - styles: - names: ${ensemble.storage.get('yyy')} children: - - Text: - styles: - names: heading-1 - text: ${device.width} - - Text: - styles: - names: heading-1 - color: ${colors.primary['900']} - text: ${ensemble.secrets.dummyOauthSecret} - - Text: - styles: - names: heading-1 - color: ${ensemble.storage.get('bbb')} - text: r@kitchenSink - - Markdown: - text: This application is built with Ensemble and serves as a guide for how to implement common UI patterns with Ensemble React. - Button: styles: - className: page-1 ${ensemble.storage.get('zzz') + '-' + ensemble.storage.get('aaa')} - label: ${productTitleName} - onTap: - executeCode: | - // Calls a function defined in test.js - sayHello(); - - - Button: - label: show error - onTap: - invokeAPI: - name: ERROR500 - onError: - showToast: - message: ${error.response.data.description} - options: - type: error - position: topRight - onResponse: - executeCode: console.log('onresponse', response) - - - DispatchButton: - events: - onFormSubmit: - executeCode: | - console.log('onresponse', data) - console.log('onresponse2', response) - - - Dropdown: - label: With the label widget - hintText: Hint Text - item-template: - data: ${app.languages} - name: language - value: ${language.languageCode} - template: - Text: - text: ${language.name + ` ( `+ language.nativeName +` )`} - onChange: - executeCode: - body: | - ensemble.storage.set('language', value) - ensemble.setLocale({languageCode: value}) - - - Text: - styles: - names: heading-1 - text: Change theme - - PopupMenu: - showDivider: true - styles: - width: 100px - backgroundColor: ${colors.primary['500']} - item-template: - data: ${app.themes} - name: theme - value: theme - template: - Text: - text: ${theme} - widget: - Icon: - name: Settings - onItemSelect: - executeCode: | - ensemble.setTheme(value.theme) - - - Text: - styles: - names: heading-1 - text: Selected Theme ${app.theme} - - - Button: - label: Theme + className: ${ensemble.storage.get('zzz') + '-' + ensemble.storage.get('aaa')} + label: ${productTitleName()} onTap: executeCode: | - console.log('>>', app.theme) - - - Divider: - - - Row: - styles: - gap: 8px - children: - - Button: - startingIcon: - name: MoreHoriz - - Button: - endingIcon: - name: MoreHoriz - - Button: - startingIcon: - name: MoreHoriz - label: start - - Button: - endingIcon: - name: MoreHoriz - label: end - - - Text: - styles: - names: heading-1 - text: Socket Example - - - Row: - styles: - gap: 8 - margin: 10px 0px - children: - - Button: - styles: - borderRadius: 20 - label: Connect Socket - onTap: - connectSocket: - name: echo - - Button: - styles: - borderRadius: 20 - label: Disconnect Socket - onTap: - disconnectSocket: - name: echo - - - Row: - styles: - gap: 8 - margin: 10px 0px - children: - - Button: - styles: - borderRadius: 20 - label: Connect Socket JS - onTap: - executeCode: - body: | - ensemble.connectSocket('echo') - onComplete: - executeCode: | - socketStatus.setText('Connected') - echo.onmessage = (e) => { - socketReceivedMessage.setText(e.data) - }; - echo.onclose = (e) => { - socketStatus.setText('Disconnected') - }; - - Button: - styles: - borderRadius: 20 - label: Disconnect Socket JS - onTap: - executeCode: | - ensemble.disconnectSocket('echo') - - - Row: - styles: - gap: 8 - margin: 10px 0px - children: - - Text: - fontWeight: 900 - text: "Socket Status :" - - - Text: - id: socketStatus - - - Row: - styles: - gap: 8 - children: - - TextInput: - id: socketMessageBox - hintText: Type message here - - - Button: - label: Send Message - onTap: - messageSocket: - name: echo - message: - msg: ${socketMessageBox.value} - x: ${ensemble.storage.get('yyy')} - - - Button: - label: Send Message JS - onTap: - executeCode: | - ensemble.messageSocket('echo', {msg: socketMessageBox.value, x: ensemble.storage.get('yyy')}) - - - Text: - styles: - fontWeight: 900 - margin: 10px 0px - text: Message from socket - - - Text: - styles: - margin: 10px 0px - id: socketReceivedMessage - - - Text: - styles: - names: heading-1 - text: Expression Examples - - - Text: - text: ${ensemble.storage.get('yyy').length>13} - - - Row: - styles: - gap: 8 - margin: 10px 0px - children: - - Text: - styles: - fontWeight: 900 - text: Simple Expression - - Text: - text: ${ensemble.storage.get('yyy')} - - - Row: - styles: - gap: 8 - margin: 10px 0px - children: - - Text: - styles: - fontWeight: 900 - text: Multiple Expression - - Text: - text: ${ensemble.storage.get('yyy')} ${ensemble.storage.get('zzz')} - - - Row: - styles: - gap: 8 - margin: 10px 0px - children: - - Text: - styles: - fontWeight: 900 - text: Expression with Strings - - Text: - text: ${ensemble.storage.get('yyy')} style - - - Row: - styles: - gap: 8 - margin: 10px 0px - children: - - Text: - styles: - fontWeight: 900 - text: Expression with mix strings - - Text: - text: ${ensemble.storage.get('yyy')} style ${ensemble.storage.get('yyy')} - - - Row: - styles: - gap: 8 - margin: 10px 0px - children: - - Text: - styles: - fontWeight: 900 - text: Expression with mix strings - - Text: - text: ${`interpolate ${ensemble.storage.get('yyy')} multiple times ${ensemble.storage.get('yyy')}`} - - - Card: - styles: - maxWidth: unset - width: unset - children: - - Markdown: - text: | - ### Slider - - Slider: - id: sliderWidget - initialValue: [28, 30] - styles: - maxWidth: unset - dots: true - min: 20 - max: 40 - value: 20 - divisions: 10 - onChange: - executeCode: | - console.log('Slider value changed: ' + value); - onComplete: - executeCode: | - console.log('Slider value completed: ' + value); - - Button: - label: checkvalue - onTap: - executeCode: | - console.log('Slider value: ' + sliderWidget.value); - - - Card: - styles: - maxWidth: unset - width: unset - children: - - Markdown: - text: | - ### Show Dialog - - Button: - label: show dialog - onTap: - showDialog: - widget: - Column: - children: - - Text: - text: i am a text - - Button: - id: unique - label: Hi I'm Button - onTap: - executeCode: |- - unique.setLoading(true) - - Button: - id: unique2 - label: Hi I'm Button Close - onTap: - executeCode: |- - unique.setLoading(!unique.loading) - - - Collapsible: - value: [2] - item-template: - data: [1, 2, 3] - name: p - template: - CollapsibleItem: - key: ${p} - label: ${'hello' + getDateLabel(p)} - children: - Text: - text: ${'world' + p + ensemble.storage.get('iconName')} - - - Text: - text: ${ensemble.storage.get('email')} - - Button: - label: Call Action - onTap: - invokeAPI: - name: getDummyProducts - onResponse: - executeCode: | - ensemble.storage.set('email', response.body.results[0].email) - ensemble.storage.set('emails', [...ensemble.storage.get("emails"),response.body.results[0].email]) - console.log('getData', response.body.results[0].email, ensemble.storage.get('emails')); - - Column: - item-template: - data: '${ensemble.storage.get("emails")}' - name: email - template: - Column: - children: - - Text: - text: ${email} - - - Carousel: - styles: - layout: multiple - indicatorPosition: bottomLeft - indicatorType: circle - indicatorColor: red - multipleItemWidthRatio: 1 - item-template: - data: ${[1, 2, 3, 4, 5]} - name: item - template: - Card: - styles: - width: auto - maxWidth: 100% - height: 200px - children: - - Text: - text: ${item} - - - Html: - cssStyles: # List of styles similar to CSS - - selector: "#hello" # or ".Hello" or "div" - properties: - # All the CSS properties, it will be entered as key value pair just as in CSS - # color: red - border: 10px solid red - borderRadius: 10px - padding: 20px - - selector: "#tag2" - properties: - id: myProperty - color: white - fontWeight: "900" - backgroundColor: red - text: | -
-

Until recently, the prevailing view assumed lorem ipsum was born as a nonsense text. “It's not Latin, though it looks like it, and it actually says nothing,” Before & After magazine answered a curious reader, “Its ‘words’ loosely approximate the frequency with which letters occur in English, which is why at a glance it looks pretty real.”

-
- - - MultiSelect: - id: multiSelect - label: Search multiple from API or Storage - placeholder: "Search or Select From Groups" - labelKey: name - valueKey: email - data: ${ensemble.storage.get('products')} - onSearch: - executeCode: | - ensemble.invokeAPI('getProducts', { search: search }).then((res) => { - const users = res?.body?.users || []; - console.log(users , "users"); - const newUsers = users.map((i) => ({ ...i, label: i.firstName + ' ' + i.lastName, name: i.firstName + ' ' + i.lastName, value: i.email })); - console.log(newUsers , "newUsers"); - ensemble.storage.set('products', newUsers); - }); - console.log("onSearch values: ", search); - onChange: - executeCode: | - console.log("onChange values: ", search); + console.log(ensemble) Global: scriptName: test.js Import: - common - -Socket: - echo: - uri: wss://echo.websocket.org/ - - onSuccess: - executeCode: | - socketStatus.setText('Connected') - - onReceive: - executeCode: | - socketReceivedMessage.setText(data) - - onDisconnect: - executeCode: | - socketStatus.setText('Disconnected') - -API: - ERROR500: - method: GET - uri: https://httpstat.us/500 - - getDummyProducts: - method: GET - cacheExpirySeconds: 10 - uri: https://randomuser.me/api/?results=1 - - getDummyNumbers: - method: GET - uri: https://661e111b98427bbbef034208.mockapi.io/number?limit=10 - onResponse: | - console.log('dummy number fetched') - - getProducts: - method: GET - inputs: - - search - uri: "https://dummyjson.com/users/search?q=${search}" diff --git a/apps/kitchen-sink/src/ensemble/scripts/common.js b/apps/kitchen-sink/src/ensemble/scripts/common.js index eb031c965..32984b1e5 100644 --- a/apps/kitchen-sink/src/ensemble/scripts/common.js +++ b/apps/kitchen-sink/src/ensemble/scripts/common.js @@ -1,5 +1,8 @@ -productTitleName = "SgrDvr"; - -getDateLabel = (val) => { - return `i am a date label ${val}`; +const productTitleName = () => { + try { + console.log(">>><<<<"); + console.log(">>>>", ensemble); + } catch (error) { + console.log(error); + } }; diff --git a/packages/framework/src/evaluate/evaluate.ts b/packages/framework/src/evaluate/evaluate.ts index 6e0be94f0..1550e4cf4 100644 --- a/packages/framework/src/evaluate/evaluate.ts +++ b/packages/framework/src/evaluate/evaluate.ts @@ -9,6 +9,48 @@ import { replace, } from "../shared"; +interface ScriptOptions extends Partial { + id?: string; +} + +export const DOMManager = (function DOMManagerFactory(): { + addScript: ( + src: string, + options?: ScriptOptions, + ) => Promise; + clearAllScripts: () => void; +} { + const allScripts = new Set(); + + return { + /** + * Adds a script element to the document + * @param src - Source URL of the script + * @param options - Optional configuration for the script element + * @returns Promise that resolves with the script element + */ + addScript: function addScript(js: string): Promise { + const script = document.createElement("script"); + + script.type = "text/javascript"; + script.textContent = js; + + allScripts.add(script); + + return new Promise((resolve, reject) => { + script.onload = (): void => resolve(script); + script.onerror = (): void => reject(new Error(`Failed to load script`)); + document.body.appendChild(script); + }); + }, + + // Clear all managed scripts + clearAllScripts: function clearAllScripts(): void { + allScripts.forEach((script) => script.remove()); + }, + }; +})(); + export const widgetStatesToInvokables = (widgets: { [key: string]: WidgetState | undefined; }): [string, InvokableMethods | undefined][] => { @@ -39,8 +81,21 @@ export const buildEvaluateFn = ( ].filter(([key, _]) => !key.includes(".")), ); + const combinedJs = ` + return myScreenScope( + () => { + return (${Object.keys(invokableObj).join(",")}) => { + ${formatJs(js)} + }; + }, + ...Object.keys(invokableObj) // Pass the entire context object + )(${Object.keys(invokableObj) + .map((key) => key) + .join(",")}); + `; + // eslint-disable-next-line @typescript-eslint/no-implied-eval, no-new-func - const jsFunc = new Function(...Object.keys(invokableObj), formatJs(js)); + const jsFunc = new Function(...Object.keys(invokableObj), combinedJs); // eslint-disable-next-line @typescript-eslint/no-unsafe-return return () => jsFunc(...Object.values(invokableObj)); diff --git a/packages/runtime/src/runtime/screen.tsx b/packages/runtime/src/runtime/screen.tsx index 123819baf..709b634bd 100644 --- a/packages/runtime/src/runtime/screen.tsx +++ b/packages/runtime/src/runtime/screen.tsx @@ -95,24 +95,28 @@ export const EnsembleScreen: React.FC = ({ const globalBlock = screen.global; const importedScripts = screen.importedScripts; - if (!globalBlock && !importedScripts) { - return; - } + const isScriptExist = document.getElementById("custom-scope-script"); - const script = document.createElement("script"); - script.type = "text/javascript"; - script.textContent = `(function () { - ${importedScripts || ""} - ${globalBlock || ""} - }).call(this)`; + const jsString = ` + const myScreenScope = function(scriptToExecute, context) { + console.log('>>>><<<<<', context); - document.body.appendChild(script); - - return () => { - if (script.parentNode) { - script.parentNode.removeChild(script); + ${importedScripts || ""} + ${globalBlock || ""} + return eval('(' + scriptToExecute.toString() + ')()'); } - }; + `; + + if (isScriptExist) { + isScriptExist.textContent = jsString; + } else { + const script = document.createElement("script"); + script.id = "custom-scope-script"; + script.type = "text/javascript"; + script.textContent = jsString; + + document.body.appendChild(script); + } }, [screen.global, screen.importedScripts]); if (!isInitialized) { From 3482779e0ba02748bd4ad4e1e3e5e2da62cb2942 Mon Sep 17 00:00:00 2001 From: sagar davara Date: Thu, 13 Feb 2025 15:10:04 +0530 Subject: [PATCH 4/9] fix: added custom scope and evaluate it --- .../src/ensemble/screens/help.yaml | 8 - .../src/ensemble/screens/home.yaml | 509 +++++++++++++++++- .../src/ensemble/scripts/common.js | 11 +- packages/framework/src/evaluate/evaluate.ts | 57 +- packages/runtime/src/runtime/screen.tsx | 9 +- 5 files changed, 518 insertions(+), 76 deletions(-) diff --git a/apps/kitchen-sink/src/ensemble/screens/help.yaml b/apps/kitchen-sink/src/ensemble/screens/help.yaml index 6b28b4012..e90cb49a7 100644 --- a/apps/kitchen-sink/src/ensemble/screens/help.yaml +++ b/apps/kitchen-sink/src/ensemble/screens/help.yaml @@ -32,14 +32,6 @@ View: styles: names: heading-1 text: Help - - Button: - styles: - className: page-1 ${ensemble.storage.get('zzz') + '-' + ensemble.storage.get('aaa')} - label: ${productTitleName} - onTap: - executeCode: | - // Calls a function defined in test.js - sayHello(); - Markdown: text: More to come! In the meantime, checkout the Ensemble [documentation](https://docs.ensembleui.com/). - Card: diff --git a/apps/kitchen-sink/src/ensemble/screens/home.yaml b/apps/kitchen-sink/src/ensemble/screens/home.yaml index 408de1ec7..f0d4a9c8a 100644 --- a/apps/kitchen-sink/src/ensemble/screens/home.yaml +++ b/apps/kitchen-sink/src/ensemble/screens/home.yaml @@ -1,21 +1,520 @@ View: onLoad: - executeCode: | - ensemble.storage.set('productTitleName', 'test 123'); + executeCode: + body: |- + ensemble.storage.set('iconName', 'home') + ensemble.storage.set('btnLoader' , false) + ensemble.storage.set('yyy' , 'page') + ensemble.storage.set('zzz' , 'button') + ensemble.storage.set('aaa' , '1') + ensemble.storage.set('bbb' , "0xffb91c1c") + ensemble.storage.set('loading', true); + console.log('>>> secret variable >>>', ensemble.secrets.dummyOauthSecret) + ensemble.storage.set('products', []); + ensemble.invokeAPI('getDummyProducts').then((res) => ensemble.storage.set('products', (res?.body?.users || []).map((i) => ({ ...i, name: i.firstName + ' ' + i.lastName })))); + const res = await ensemble.invokeAPI('getDummyNumbers') + await new Promise((resolve) => setTimeout(resolve, 5000)) + return res + onComplete: + executeCode: | + console.log('API triggered', result) + + header: + title: + Header: + + styles: + className: topView + scrollableView: true + backgroundColor: ${colors.primary['200']} body: Column: + styles: + names: ${ensemble.storage.get('yyy')} children: + - Text: + styles: + names: heading-1 + text: ${device.width} + - Text: + styles: + names: heading-1 + color: ${colors.primary['900']} + text: ${ensemble.secrets.dummyOauthSecret} + - Text: + styles: + names: heading-1 + color: ${ensemble.storage.get('bbb')} + text: r@kitchenSink + - Markdown: + text: This application is built with Ensemble and serves as a guide for how to implement common UI patterns with Ensemble React. - Button: styles: - className: ${ensemble.storage.get('zzz') + '-' + ensemble.storage.get('aaa')} - label: ${productTitleName()} + className: page-1 ${ensemble.storage.get('zzz') + '-' + ensemble.storage.get('aaa')} + label: ${productTitleName} + onTap: + executeCode: | + // Calls a function defined in test.js + sayHello(); + + - Button: + label: show error + onTap: + invokeAPI: + name: ERROR500 + onError: + showToast: + message: ${error.response.data.description} + options: + type: error + position: topRight + onResponse: + executeCode: console.log('onresponse', response) + + - DispatchButton: + events: + onFormSubmit: + executeCode: | + console.log('onresponse', data) + console.log('onresponse2', response) + + - Dropdown: + label: With the label widget + hintText: Hint Text + item-template: + data: ${app.languages} + name: language + value: ${language.languageCode} + template: + Text: + text: ${language.name + ` ( `+ language.nativeName +` )`} + onChange: + executeCode: + body: | + ensemble.storage.set('language', value) + ensemble.setLocale({languageCode: value}) + + - Text: + styles: + names: heading-1 + text: Change theme + - PopupMenu: + showDivider: true + styles: + width: 100px + backgroundColor: ${colors.primary['500']} + item-template: + data: ${app.themes} + name: theme + value: theme + template: + Text: + text: ${theme} + widget: + Icon: + name: Settings + onItemSelect: + executeCode: | + ensemble.setTheme(value.theme) + + - Text: + styles: + names: heading-1 + text: Selected Theme ${app.theme} + + - Button: + label: Theme onTap: executeCode: | - console.log(ensemble) + console.log('>>', app.theme) + + - Divider: + + - Row: + styles: + gap: 8px + children: + - Button: + startingIcon: + name: MoreHoriz + - Button: + endingIcon: + name: MoreHoriz + - Button: + startingIcon: + name: MoreHoriz + label: start + - Button: + endingIcon: + name: MoreHoriz + label: end + + - Text: + styles: + names: heading-1 + text: Socket Example + + - Row: + styles: + gap: 8 + margin: 10px 0px + children: + - Button: + styles: + borderRadius: 20 + label: Connect Socket + onTap: + connectSocket: + name: echo + - Button: + styles: + borderRadius: 20 + label: Disconnect Socket + onTap: + disconnectSocket: + name: echo + + - Row: + styles: + gap: 8 + margin: 10px 0px + children: + - Button: + styles: + borderRadius: 20 + label: Connect Socket JS + onTap: + executeCode: + body: | + ensemble.connectSocket('echo') + onComplete: + executeCode: | + socketStatus.setText('Connected') + echo.onmessage = (e) => { + socketReceivedMessage.setText(e.data) + }; + echo.onclose = (e) => { + socketStatus.setText('Disconnected') + }; + - Button: + styles: + borderRadius: 20 + label: Disconnect Socket JS + onTap: + executeCode: | + ensemble.disconnectSocket('echo') + + - Row: + styles: + gap: 8 + margin: 10px 0px + children: + - Text: + fontWeight: 900 + text: "Socket Status :" + + - Text: + id: socketStatus + + - Row: + styles: + gap: 8 + children: + - TextInput: + id: socketMessageBox + hintText: Type message here + + - Button: + label: Send Message + onTap: + messageSocket: + name: echo + message: + msg: ${socketMessageBox.value} + x: ${ensemble.storage.get('yyy')} + + - Button: + label: Send Message JS + onTap: + executeCode: | + ensemble.messageSocket('echo', {msg: socketMessageBox.value, x: ensemble.storage.get('yyy')}) + + - Text: + styles: + fontWeight: 900 + margin: 10px 0px + text: Message from socket + + - Text: + styles: + margin: 10px 0px + id: socketReceivedMessage + + - Text: + styles: + names: heading-1 + text: Expression Examples + + - Text: + text: ${ensemble.storage.get('yyy').length>13} + + - Row: + styles: + gap: 8 + margin: 10px 0px + children: + - Text: + styles: + fontWeight: 900 + text: Simple Expression + - Text: + text: ${ensemble.storage.get('yyy')} + + - Row: + styles: + gap: 8 + margin: 10px 0px + children: + - Text: + styles: + fontWeight: 900 + text: Multiple Expression + - Text: + text: ${ensemble.storage.get('yyy')} ${ensemble.storage.get('zzz')} + + - Row: + styles: + gap: 8 + margin: 10px 0px + children: + - Text: + styles: + fontWeight: 900 + text: Expression with Strings + - Text: + text: ${ensemble.storage.get('yyy')} style + + - Row: + styles: + gap: 8 + margin: 10px 0px + children: + - Text: + styles: + fontWeight: 900 + text: Expression with mix strings + - Text: + text: ${ensemble.storage.get('yyy')} style ${ensemble.storage.get('yyy')} + + - Row: + styles: + gap: 8 + margin: 10px 0px + children: + - Text: + styles: + fontWeight: 900 + text: Expression with mix strings + - Text: + text: ${`interpolate ${ensemble.storage.get('yyy')} multiple times ${ensemble.storage.get('yyy')}`} + + - Card: + styles: + maxWidth: unset + width: unset + children: + - Markdown: + text: | + ### Slider + - Slider: + id: sliderWidget + initialValue: [28, 30] + styles: + maxWidth: unset + dots: true + min: 20 + max: 40 + value: 20 + divisions: 10 + onChange: + executeCode: | + console.log('Slider value changed: ' + value); + onComplete: + executeCode: | + console.log('Slider value completed: ' + value); + - Button: + label: checkvalue + onTap: + executeCode: | + console.log('Slider value: ' + sliderWidget.value); + + - Card: + styles: + maxWidth: unset + width: unset + children: + - Markdown: + text: | + ### Show Dialog + - Button: + label: show dialog + onTap: + showDialog: + widget: + Column: + children: + - Text: + text: i am a text + - Button: + id: unique + label: Hi I'm Button + onTap: + executeCode: |- + unique.setLoading(true) + - Button: + id: unique2 + label: Hi I'm Button Close + onTap: + executeCode: |- + unique.setLoading(!unique.loading) + + - Collapsible: + value: [2] + item-template: + data: [1, 2, 3] + name: p + template: + CollapsibleItem: + key: ${p} + label: ${'hello' + getDateLabel(p)} + children: + Text: + text: ${'world' + p + ensemble.storage.get('iconName')} + + - Text: + text: ${ensemble.storage.get('email')} + - Button: + label: Call Action + onTap: + invokeAPI: + name: getDummyProducts + onResponse: + executeCode: | + ensemble.storage.set('email', response.body.results[0].email) + ensemble.storage.set('emails', [...ensemble.storage.get("emails"),response.body.results[0].email]) + console.log('getData', response.body.results[0].email, ensemble.storage.get('emails')); + - Column: + item-template: + data: '${ensemble.storage.get("emails")}' + name: email + template: + Column: + children: + - Text: + text: ${email} + + - Carousel: + styles: + layout: multiple + indicatorPosition: bottomLeft + indicatorType: circle + indicatorColor: red + multipleItemWidthRatio: 1 + item-template: + data: ${[1, 2, 3, 4, 5]} + name: item + template: + Card: + styles: + width: auto + maxWidth: 100% + height: 200px + children: + - Text: + text: ${item} + + - Html: + cssStyles: # List of styles similar to CSS + - selector: "#hello" # or ".Hello" or "div" + properties: + # All the CSS properties, it will be entered as key value pair just as in CSS + # color: red + border: 10px solid red + borderRadius: 10px + padding: 20px + - selector: "#tag2" + properties: + id: myProperty + color: white + fontWeight: "900" + backgroundColor: red + text: | +
+

Until recently, the prevailing view assumed lorem ipsum was born as a nonsense text. “It's not Latin, though it looks like it, and it actually says nothing,” Before & After magazine answered a curious reader, “Its ‘words’ loosely approximate the frequency with which letters occur in English, which is why at a glance it looks pretty real.”

+
+ + - MultiSelect: + id: multiSelect + label: Search multiple from API or Storage + placeholder: "Search or Select From Groups" + labelKey: name + valueKey: email + data: ${ensemble.storage.get('products')} + onSearch: + executeCode: | + ensemble.invokeAPI('getProducts', { search: search }).then((res) => { + const users = res?.body?.users || []; + console.log(users , "users"); + const newUsers = users.map((i) => ({ ...i, label: i.firstName + ' ' + i.lastName, name: i.firstName + ' ' + i.lastName, value: i.email })); + console.log(newUsers , "newUsers"); + ensemble.storage.set('products', newUsers); + }); + console.log("onSearch values: ", search); + onChange: + executeCode: | + console.log("onChange values: ", search); Global: scriptName: test.js Import: - common + +Socket: + echo: + uri: wss://echo.websocket.org/ + + onSuccess: + executeCode: | + socketStatus.setText('Connected') + + onReceive: + executeCode: | + socketReceivedMessage.setText(data) + + onDisconnect: + executeCode: | + socketStatus.setText('Disconnected') + +API: + ERROR500: + method: GET + uri: https://httpstat.us/500 + + getDummyProducts: + method: GET + cacheExpirySeconds: 10 + uri: https://randomuser.me/api/?results=1 + + getDummyNumbers: + method: GET + uri: https://661e111b98427bbbef034208.mockapi.io/number?limit=10 + onResponse: | + console.log('dummy number fetched') + + getProducts: + method: GET + inputs: + - search + uri: "https://dummyjson.com/users/search?q=${search}" diff --git a/apps/kitchen-sink/src/ensemble/scripts/common.js b/apps/kitchen-sink/src/ensemble/scripts/common.js index 32984b1e5..ecfca0e07 100644 --- a/apps/kitchen-sink/src/ensemble/scripts/common.js +++ b/apps/kitchen-sink/src/ensemble/scripts/common.js @@ -1,8 +1,5 @@ -const productTitleName = () => { - try { - console.log(">>><<<<"); - console.log(">>>>", ensemble); - } catch (error) { - console.log(error); - } +const productTitleName = "SgrDvr"; + +getDateLabel = (val) => { + return `i am a date label ${val}`; }; diff --git a/packages/framework/src/evaluate/evaluate.ts b/packages/framework/src/evaluate/evaluate.ts index 1550e4cf4..1613bd88e 100644 --- a/packages/framework/src/evaluate/evaluate.ts +++ b/packages/framework/src/evaluate/evaluate.ts @@ -9,48 +9,6 @@ import { replace, } from "../shared"; -interface ScriptOptions extends Partial { - id?: string; -} - -export const DOMManager = (function DOMManagerFactory(): { - addScript: ( - src: string, - options?: ScriptOptions, - ) => Promise; - clearAllScripts: () => void; -} { - const allScripts = new Set(); - - return { - /** - * Adds a script element to the document - * @param src - Source URL of the script - * @param options - Optional configuration for the script element - * @returns Promise that resolves with the script element - */ - addScript: function addScript(js: string): Promise { - const script = document.createElement("script"); - - script.type = "text/javascript"; - script.textContent = js; - - allScripts.add(script); - - return new Promise((resolve, reject) => { - script.onload = (): void => resolve(script); - script.onerror = (): void => reject(new Error(`Failed to load script`)); - document.body.appendChild(script); - }); - }, - - // Clear all managed scripts - clearAllScripts: function clearAllScripts(): void { - allScripts.forEach((script) => script.remove()); - }, - }; -})(); - export const widgetStatesToInvokables = (widgets: { [key: string]: WidgetState | undefined; }): [string, InvokableMethods | undefined][] => { @@ -81,17 +39,12 @@ export const buildEvaluateFn = ( ].filter(([key, _]) => !key.includes(".")), ); + const args = Object.keys(invokableObj).join(","); + const combinedJs = ` - return myScreenScope( - () => { - return (${Object.keys(invokableObj).join(",")}) => { - ${formatJs(js)} - }; - }, - ...Object.keys(invokableObj) // Pass the entire context object - )(${Object.keys(invokableObj) - .map((key) => key) - .join(",")}); + return myScreenScope(() => { + ${formatJs(js)}; + }, {${args}}); `; // eslint-disable-next-line @typescript-eslint/no-implied-eval, no-new-func diff --git a/packages/runtime/src/runtime/screen.tsx b/packages/runtime/src/runtime/screen.tsx index 709b634bd..191f934bf 100644 --- a/packages/runtime/src/runtime/screen.tsx +++ b/packages/runtime/src/runtime/screen.tsx @@ -99,11 +99,12 @@ export const EnsembleScreen: React.FC = ({ const jsString = ` const myScreenScope = function(scriptToExecute, context) { - console.log('>>>><<<<<', context); + with (context) { + ${importedScripts || ""} + ${globalBlock || ""} - ${importedScripts || ""} - ${globalBlock || ""} - return eval('(' + scriptToExecute.toString() + ')()'); + return eval('(' + scriptToExecute.toString() + ')()'); + } } `; From 4a7d9d0ac8f23916f07f906438e6dbe5ecbd70f8 Mon Sep 17 00:00:00 2001 From: sagar davara Date: Tue, 18 Feb 2025 22:00:20 +0530 Subject: [PATCH 5/9] fix: working on import scripts --- apps/kitchen-sink/src/ensemble/scripts/common.js | 2 +- packages/framework/src/evaluate/evaluate.ts | 8 ++++++++ packages/runtime/src/runtime/screen.tsx | 6 +++--- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/apps/kitchen-sink/src/ensemble/scripts/common.js b/apps/kitchen-sink/src/ensemble/scripts/common.js index ecfca0e07..cb0cb5c7a 100644 --- a/apps/kitchen-sink/src/ensemble/scripts/common.js +++ b/apps/kitchen-sink/src/ensemble/scripts/common.js @@ -1,5 +1,5 @@ const productTitleName = "SgrDvr"; -getDateLabel = (val) => { +const getDateLabel = (val) => { return `i am a date label ${val}`; }; diff --git a/packages/framework/src/evaluate/evaluate.ts b/packages/framework/src/evaluate/evaluate.ts index 1613bd88e..b232bd191 100644 --- a/packages/framework/src/evaluate/evaluate.ts +++ b/packages/framework/src/evaluate/evaluate.ts @@ -19,6 +19,10 @@ export const widgetStatesToInvokables = (widgets: { }); }; +interface InvokableWindow extends Window { + [key: string]: unknown; +} + export const buildEvaluateFn = ( screen: Partial, js?: string, @@ -47,6 +51,10 @@ export const buildEvaluateFn = ( }, {${args}}); `; + Object.entries(invokableObj).forEach(([key, value]) => { + (window as unknown as InvokableWindow)[key] = value; + }); + // eslint-disable-next-line @typescript-eslint/no-implied-eval, no-new-func const jsFunc = new Function(...Object.keys(invokableObj), combinedJs); diff --git a/packages/runtime/src/runtime/screen.tsx b/packages/runtime/src/runtime/screen.tsx index 191f934bf..9dffc570e 100644 --- a/packages/runtime/src/runtime/screen.tsx +++ b/packages/runtime/src/runtime/screen.tsx @@ -98,11 +98,11 @@ export const EnsembleScreen: React.FC = ({ const isScriptExist = document.getElementById("custom-scope-script"); const jsString = ` + ${importedScripts || ""} + ${globalBlock || ""} + const myScreenScope = function(scriptToExecute, context) { with (context) { - ${importedScripts || ""} - ${globalBlock || ""} - return eval('(' + scriptToExecute.toString() + ')()'); } } From cc73007b3650da853eecf84df992baa8ed9c4110 Mon Sep 17 00:00:00 2001 From: sagar davara Date: Wed, 19 Feb 2025 00:19:36 +0530 Subject: [PATCH 6/9] fix: added js in closures --- packages/framework/src/evaluate/evaluate.ts | 13 ++++++------- packages/runtime/src/runtime/screen.tsx | 14 ++++++++------ 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/packages/framework/src/evaluate/evaluate.ts b/packages/framework/src/evaluate/evaluate.ts index b232bd191..f1638cee9 100644 --- a/packages/framework/src/evaluate/evaluate.ts +++ b/packages/framework/src/evaluate/evaluate.ts @@ -44,17 +44,16 @@ export const buildEvaluateFn = ( ); const args = Object.keys(invokableObj).join(","); + Object.keys(invokableObj).forEach((key) => { + (window as unknown as InvokableWindow)[key] = invokableObj[key]; + }); const combinedJs = ` - return myScreenScope(() => { - ${formatJs(js)}; - }, {${args}}); + return evalInClosure(() => { + ${formatJs(js)} + }, {${args}}) `; - Object.entries(invokableObj).forEach(([key, value]) => { - (window as unknown as InvokableWindow)[key] = value; - }); - // eslint-disable-next-line @typescript-eslint/no-implied-eval, no-new-func const jsFunc = new Function(...Object.keys(invokableObj), combinedJs); diff --git a/packages/runtime/src/runtime/screen.tsx b/packages/runtime/src/runtime/screen.tsx index 9dffc570e..e74c75591 100644 --- a/packages/runtime/src/runtime/screen.tsx +++ b/packages/runtime/src/runtime/screen.tsx @@ -98,14 +98,16 @@ export const EnsembleScreen: React.FC = ({ const isScriptExist = document.getElementById("custom-scope-script"); const jsString = ` - ${importedScripts || ""} - ${globalBlock || ""} - - const myScreenScope = function(scriptToExecute, context) { - with (context) { + const createEvalClosure = () => { + ${importedScripts || ""} + ${globalBlock || ""} + + return (scriptToExecute, context) => { return eval('(' + scriptToExecute.toString() + ')()'); - } + } } + + const evalInClosure = createEvalClosure() `; if (isScriptExist) { From 387d3357d0832d7734927a3e9cef7ef6dea08753 Mon Sep 17 00:00:00 2001 From: sagar davara Date: Thu, 20 Feb 2025 02:30:13 +0530 Subject: [PATCH 7/9] fix: initialize ensemble before script mounting --- packages/runtime/src/runtime/screen.tsx | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/packages/runtime/src/runtime/screen.tsx b/packages/runtime/src/runtime/screen.tsx index e74c75591..1c9df37a9 100644 --- a/packages/runtime/src/runtime/screen.tsx +++ b/packages/runtime/src/runtime/screen.tsx @@ -98,6 +98,19 @@ export const EnsembleScreen: React.FC = ({ const isScriptExist = document.getElementById("custom-scope-script"); const jsString = ` + // Create a base object and pin its reference + const ensembleObj = {}; + Object.defineProperty(window, 'ensemble', { + get: () => ensembleObj, + set: (value) => { + // Copy properties instead of replacing reference + Object.assign(ensembleObj, value); + return true; + }, + configurable: true, + enumerable: true + }); + const createEvalClosure = () => { ${importedScripts || ""} ${globalBlock || ""} From 68c03ad990eacd5efc2ecda88e16484c18931ffb Mon Sep 17 00:00:00 2001 From: sagar davara Date: Thu, 20 Feb 2025 22:32:54 +0530 Subject: [PATCH 8/9] fix: define ensemble before mounting --- packages/framework/src/evaluate/evaluate.ts | 2 +- packages/runtime/src/runtime/screen.tsx | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/framework/src/evaluate/evaluate.ts b/packages/framework/src/evaluate/evaluate.ts index f1638cee9..604765d5b 100644 --- a/packages/framework/src/evaluate/evaluate.ts +++ b/packages/framework/src/evaluate/evaluate.ts @@ -49,7 +49,7 @@ export const buildEvaluateFn = ( }); const combinedJs = ` - return evalInClosure(() => { + return evalInClosure((${args}) => { ${formatJs(js)} }, {${args}}) `; diff --git a/packages/runtime/src/runtime/screen.tsx b/packages/runtime/src/runtime/screen.tsx index 1c9df37a9..b54d0afac 100644 --- a/packages/runtime/src/runtime/screen.tsx +++ b/packages/runtime/src/runtime/screen.tsx @@ -116,7 +116,8 @@ export const EnsembleScreen: React.FC = ({ ${globalBlock || ""} return (scriptToExecute, context) => { - return eval('(' + scriptToExecute.toString() + ')()'); + const args = Object.keys(context).join(","); + return eval('(' + scriptToExecute.toString() + ')('+ args +')'); } } From 39974db4e1f82f6c0d6198052e107247227ddcbf Mon Sep 17 00:00:00 2001 From: sagar davara Date: Mon, 24 Feb 2025 18:41:21 +0530 Subject: [PATCH 9/9] fix: improve context and window.ensemble update logic --- packages/framework/src/evaluate/evaluate.ts | 25 ++++++++++++++++----- packages/runtime/src/runtime/screen.tsx | 5 +++-- 2 files changed, 23 insertions(+), 7 deletions(-) diff --git a/packages/framework/src/evaluate/evaluate.ts b/packages/framework/src/evaluate/evaluate.ts index 604765d5b..eba7f3dba 100644 --- a/packages/framework/src/evaluate/evaluate.ts +++ b/packages/framework/src/evaluate/evaluate.ts @@ -1,4 +1,12 @@ -import { isEmpty, merge, toString } from "lodash-es"; +import { + get, + has, + isEmpty, + isUndefined, + merge, + omitBy, + toString, +} from "lodash-es"; import type { ScreenContextDefinition } from "../state/screen"; import type { InvokableMethods, WidgetState } from "../state/widget"; import { @@ -43,13 +51,20 @@ export const buildEvaluateFn = ( ].filter(([key, _]) => !key.includes(".")), ); + if (has(invokableObj, "ensemble")) { + const tempEnsemble = get(invokableObj, "ensemble") as { + [key: string]: unknown; + }; + (window as unknown as InvokableWindow).ensemble = omitBy( + tempEnsemble, + isUndefined, + ); + } + const args = Object.keys(invokableObj).join(","); - Object.keys(invokableObj).forEach((key) => { - (window as unknown as InvokableWindow)[key] = invokableObj[key]; - }); const combinedJs = ` - return evalInClosure((${args}) => { + return evalInClosure(() => { ${formatJs(js)} }, {${args}}) `; diff --git a/packages/runtime/src/runtime/screen.tsx b/packages/runtime/src/runtime/screen.tsx index b54d0afac..58017c476 100644 --- a/packages/runtime/src/runtime/screen.tsx +++ b/packages/runtime/src/runtime/screen.tsx @@ -116,8 +116,9 @@ export const EnsembleScreen: React.FC = ({ ${globalBlock || ""} return (scriptToExecute, context) => { - const args = Object.keys(context).join(","); - return eval('(' + scriptToExecute.toString() + ')('+ args +')'); + with (context) { + return eval('(' + scriptToExecute.toString() + ')()'); + } } }