Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add example using JS framework (e.g. React, Vue) #124

Open
neroist opened this issue Jun 22, 2023 · 17 comments
Open

Add example using JS framework (e.g. React, Vue) #124

neroist opened this issue Jun 22, 2023 · 17 comments
Labels
documentation Improvements or additions to documentation

Comments

@neroist
Copy link
Member

neroist commented Jun 22, 2023

No description provided.

@delphius
Copy link

delphius commented Jun 22, 2023

As far as I understand, there is no writing practice and a good coding style for webui yet. When using libraries like webui in real world projects, you need to take into account, that the mechanisms of interlanguage interactions provided by them in multilingual programming (polyglot programming) do not allow you to combine policies (what a program should do) with an interaction protocol (what it can do) without imposing any restrictions.

There should be a diagram illustrating the capabilities of webui

Taking, as a framework for the development of presentation components, for example, React, several approaches to the implementation of a scheme of such interaction are seen. When using any approach, the output is the same product, its choice will be largely determined by the level of knowledge in front-end development.

1. Server Side Generation

The first option is backend generation, when all page (form) html+css+js code every time is generated and sent to the browser from the target language. In this case, the server part acts as a static site generator, similar to Jekyll, forming entire pages on the fly depending on the context and sending them to the browser via webui.

React features on the form:
Application on the browser side is practically meaningless, because its main ability to manage the display and lifecycle of components on a page without reloading it is not used.

Features of React components:
JSX components (not necessarily React components, but any similar structures that, using syntactic sugar and a combination of style, markup and code in one place, turn visual dynamic objects into full-fledged reused stateful classes, see for example Mitosis project), it is quite realistic to use when building a page on the backend side, storing them in the form of jsx files on disk/in the database/inside the program and using a template processor like mustache to fill in data in components. From these components, it is much easier to render a static page on the server side and, in general, it is much easier to work with such structures.

Required level of knowledge:
Knowledge of only the backend programming language and the basics of html+css is required.

2. Static Components Generation

The second option is when the bootstrap code of the current form is sent from the programming language, and the form itself is a combination of html + css + js with named (id) elements that act as component mounting nodes. The lifecycle management of components (whether it is React jsx components, or just code generation of DOM nodes) is carried out on the side of the programming language, mounting, unmounting and updating based on user input results is the concern of the backend. This option is closest to the mvvm paradigm, where the browser acts only as a V (view), which fits perfectly into the restrictions imposed by webui and very close to the role of forms and components on them in conventional desktop applications.

React features on the form:
Since there is no complete reload of the browser page with each state update, it is quite possible to use React to render components on the page, but the start of rendering, like the rest of the component lifecycle, is controlled and launched from the backend via webui.

React component Capabilities:
As in the first case, the components are fully formed on the backend and assembled and sent to the browser for rendering using React.

Required level of knowledge:
Requires knowledge of the backend programming language, confident knowledge of html+css, javascript and React basics.

3. Dynamic Components Generation

The third option, respectively, is dynamic components generation. The only difference from the second option is that the components have a state and behavior, they cease to be pure functions, and this option may also be useful, but here you need to be careful that the business logic does not get out of the backend and in order not to lose full control of the backend over the frontend because of its excessive complexity. The state/behaviour should, logically, concern only the appearance (V) of the component, the request for the data required to form the component render, transmitting user input to the backend, as well as the implementation of a two-way data binding mechanism and events through, for example, the publisher subscriber model or the event bus.

React features on the form:
React fully manages the lifecycle of components, the backend only initiates mounting or unmounting, as well as changing the state of components, gives the necessary data to components upon their request, and also interacts with them through a two-way binding mechanism. In this variant, all the features of React are used to the maximum.

React component Capabilities:
The components are available to React from the code on the browser side, the missing data for formation is pulled from the backend. All the capabilities of the components in terms of state and behavior are fully involved.

Required level of knowledge:
Requires knowledge of the backend programming language, confident knowledge of html+css, javascript and React.

To illustrate what has been said, let's take the example offered by React and implement it, for example, in the first variant (SSG) with the elements of the second one (node replacement on update):

unit productlist;

{$mode delphi}{$H+}

interface

uses
  WebUI, SysUtils;

type
  TProduct = record
    category: string;
    price: string;
    stocked: Boolean;
    name: string;
  end;

  TProductList = array of TProduct;

  TFilterableProductTable = class
  private
    FProducts: TProductList;
    FFilterText: string;
    FInStockOnly: Boolean;
    FWindow: size_t;

    procedure InitializeWindow;
    procedure BindEvents;
    procedure ShowView;
    procedure HandleFilterTextChange(e: PWebUI_event_t);
    procedure HandleInStockOnlyChange(e: PWebUI_event_t);
    function FilterProducts: TProductList;
    procedure RenderProductTable(products: TProductList);
    function GetProductCategoryRow(category: string): string;
    function GetProductRow(product: TProduct): string;
  public
    constructor Create(products: TProductList);
    procedure Run;
  end;

implementation

var
  FilterableTableInstance: TFilterableProductTable;

procedure FilterTextChangeWrapper(e: PWebUI_event_t);
begin
  FilterableTableInstance.HandleFilterTextChange(e);
end;

procedure InStockOnlyChangeWrapper(e: PWebUI_event_t);
begin
  FilterableTableInstance.HandleInStockOnlyChange(e);
end;

constructor TFilterableProductTable.Create(products: TProductList);
begin
  FProducts := products;
  FFilterText := '';
  FInStockOnly := False;
  InitializeWindow;
  BindEvents;
end;

procedure TFilterableProductTable.InitializeWindow;
begin
  FWindow := webui_new_window;
end;

procedure TFilterableProductTable.BindEvents;
begin
  FilterableTableInstance := Self;
  webui_bind(FWindow, 'FilterTextChange', @FilterTextChangeWrapper);
  webui_bind(FWindow, 'InStockOnlyChange', @InStockOnlyChangeWrapper);
end;

procedure TFilterableProductTable.ShowView;

function IfThen(AValue: Boolean; const ATrue: string; const AFalse: string = ''): string;
begin
  if AValue then
    Result := ATrue
  else
    Result := AFalse;
end;

begin
  webui_show(FWindow, PChar(
    '<!DOCTYPE html><html><head></head><body><div>' +
    '<input type="text" value="' + FFilterText + '" onkeyup="webui_fn(`FilterTextChange`, event.target.value); console.log(event.target.value)">' +
    '<label><input type="checkbox" ' + IfThen(FInStockOnly, 'checked') + ' onclick="webui_fn(`InStockOnlyChange`, event.target.checked); console.log(event.target.checked)"> Only show products in stock</label>' +
    '</div>' +
    '<table>' +
    '<thead><tr><th>Name</th><th>Price</th></tr></thead>' +
    '<tbody id="productTableBody"></tbody>' +
    '</table></body></html>'));
end;

procedure TFilterableProductTable.HandleFilterTextChange(e: PWebUI_event_t);
begin
  FFilterText := webui_get_string(e);
  RenderProductTable(FilterProducts);
end;

procedure TFilterableProductTable.HandleInStockOnlyChange(e: PWebUI_event_t);
begin
  FInStockOnly := webui_get_bool(e);
  RenderProductTable(FilterProducts);
end;

function TFilterableProductTable.FilterProducts: TProductList;
var
  filteredList: TProductList;
  product: TProduct;
begin
  SetLength(filteredList, 0);
  for product in FProducts do
  begin
    if (FFilterText = '') or (Pos(LowerCase(FFilterText), LowerCase(product.name)) > 0) then
    begin
      if not FInStockOnly or (FInStockOnly and product.stocked) then
      begin
        SetLength(filteredList, Length(filteredList) + 1);
        filteredList[Length(filteredList) - 1] := product;
      end;
    end;
  end;
  Result := filteredList;
end;

procedure TFilterableProductTable.RenderProductTable(products: TProductList);
var
  tableBody: string;
  product: TProduct;
  currentCategory: string;
begin
  tableBody := '';
  currentCategory := '';
  for product in products do
  begin
    if product.category <> currentCategory then
    begin
      tableBody := tableBody + GetProductCategoryRow(product.category);
      currentCategory := product.category;
    end;
    tableBody := tableBody + GetProductRow(product);
  end;
  tableBody := 'document.getElementById("productTableBody").innerHTML = `' + tableBody + '`';
  webui_run(FWindow, PChar(tableBody));
end;

function TFilterableProductTable.GetProductCategoryRow(category: string): string;
begin
  Result := '<tr><th colspan="2">' + category + '</th></tr>';
end;

function TFilterableProductTable.GetProductRow(product: TProduct): string;
var
  name: string;
begin
  if product.stocked then
    name := product.name
  else
    name := '<span style="color: red;">' + product.name + '</span>';
  Result := '<tr><td>' + name + '</td><td>' + product.price + '</td></tr>';
end;

procedure TFilterableProductTable.Run;
begin
  ShowView;
  webui_wait;
end;

end.

Use case program:

program reactexample;

{$mode objfpc}{$H+}

uses
  productlist;

var
  Products: TProductList;
  Product: TProduct;
  FilterableProductTable: TFilterableProductTable;

begin
SetLength(Products, 6);

  Product.Category := 'Fruits';
  Product.Price := '$1';
  Product.Stocked := True;
  Product.Name := 'Apple';
  Products[0] := Product;

  Product.Category := 'Fruits';
  Product.Price := '$1';
  Product.Stocked := True;
  Product.Name := 'Dragonfruit';
  Products[1] := Product;

  Product.Category := 'Fruits';
  Product.Price := '$2';
  Product.Stocked := False;
  Product.Name := 'Passionfruit';
  Products[2] := Product;

  Product.Category := 'Vegetables';
  Product.Price := '$2';
  Product.Stocked := True;
  Product.Name := 'Spinach';
  Products[3] := Product;

  Product.Category := 'Vegetables';
  Product.Price := '$4';
  Product.Stocked := False;
  Product.Name := 'Pumpkin';
  Products[4] := Product;

  Product.Category := 'Vegetables';
  Product.Price := '$1';
  Product.Stocked := True;
  Product.Name := 'Peas';
  Products[5] := Product;

  FilterableProductTable := TFilterableProductTable.Create(Products);
  FilterableProductTable.Run;

  FilterableProductTable.Free;
end.

I am a pascalist, so the example is in free pascal, but no one prevents transpiling this example to any other programming language. Due to limitations, the current version of free pascal compiler does not yet have anonymous functions, and there is no possibility of passing the procedure of object as a procedure, as required by webui, for binding, so I had to insert a crutch in the form of a state variable. I know that it is possible to make a singleton, but it will still be the same. That's all, in other programming languages this problem will most likely not be, and in free pascal it will soon also be solved with, for example, anonymous functions. Trunk already has it.

This example does not reveal the use of webui specifically with react, but gives an approximate understanding of how things are, what options there are and where to move on.

There is an example of third approach

There is also a fourth approach, but it is the limiting state of the third, when all or almost all of the business logic is on the frontend, as shown, for example, in the final implementation of React example, which we have just implemented in free pascal via webui, but in the opposite style. Backend in this case, can act as just a data source (repository), taking into account the same restrictions and communication methods that are provided by webui. But this is already moving away from the original idea of webui, where the browser is just a representation (form) of a desktop application written in any programming language, but it has a place to be.

This approach may include such a promising direction as code transpilation in js or webassembly. The meaning of the approach will be to write the event handler code in the target programming language, and when compiling the application, all the code related to the browser will be transpiled and transferred to the frontend. But this approach requires additional work on the formation of unified boiler plates for webui and their automated processing for each programming language.

In any case, on the browser side, with the help of a huge javascript ecosystem, it is advisable to solve tasks that for some reason (lack of a library, limitations of the language/compiler/operating system, etc.) are expensive, difficult or impossible to implement in a programming language. A concrete example can be web scraping. What is the point of using such proven methods as Selenium (webdriver) or more modern analogues on the backend to receive and process data from Internet sources, if the full power of modern desktop browsers is in your hands with webui?

And here the second question arises that the study of javascript and its ecosystem, its limitations and capabilities, is a primary task for a programmer writing a backend in any programming language and using the browser via webui, or similar library, for the frontend. This greatly expands the set, understanding and applicability of certain solutions for specific projects. And this is a synergistic property of polyglot programming, when the final solution uses a combination of the best features of languages.

Everything we talked about mainly concerns the development of web application interfaces, and it may not be very clear to programmers who just want to continue making forms with buttons and checkboxes as they usually did for desktop applications. But even for such fastidious people there is a good solution that completely fits into the scheme that we discussed above. The only thing is, we will now call such applications not desktop, but Rich Web Applications or RIA. And for our purposes, instead of React and JSX components, we can use Qooxdoo JavaScript Framework and its wonderful widgets. The simplest example of use with webui can be viewed here

Please forgive my English, I use machine translation.

@hassandraga
Copy link
Member

Thank you, @delphius, for this interesting article. Yes, as you said, there are three approaches, Server Side Generation, Static Components Generation, Dynamic Components Generation, And maybe a fourth approach where the backend acts mainly as a data source. I look forward to writing examples in one or all approaches when I have free time. Maybe this will require more APIs in the WebUI. Let's see.

@hassandraga
Copy link
Member

I will try adding an example soon...

@jinzhongjia
Copy link
Contributor

Examples can be added, but I think webui should not become an overly heavy library, is should maintain simplicity

@AlbertShown
Copy link
Contributor

This needs to add a new example I guess, while webui will be untouched.

@zhuewizz
Copy link

vue and react are commonly used frameworks at present, I use webui to collect data locally, go calls js to present data, but with ui.run and ui.Script, it always does not work, there is little information about webui, please have some examples as soon as possible, thank you

@AlbertShown
Copy link
Contributor

ui.run and ui.Script, it always does not work

Did you added webui.js to your UI?

@laoshaw
Copy link

laoshaw commented Apr 7, 2024

one typical use case could be:

  • a few html/css/js files under a directory(be it from react/vue/svelte/manually-created) that webui can load the whole directory and start with its index.html, this is basically a root directory of your frontend as in any browser based project.
  • some demo code shows javascript calls backend code to do some work, and update the web page with returned value. e.g. a RESTful fetch() to a C program on the backend. This is like a cgi-call though it's websocket behind-the-scene.
  • since websocket is bi-directional, it's even better to show its two-day communication capability in the demo.

the existing examples are great, but we need something a little more complex and "practical"

@binarytide
Copy link

ideally I'd love to just be able to point webUI at a folder containing a built copy of my vue app. serving the static assets within the same script doesn't seem to work.

@AlbertShown
Copy link
Contributor

@binarytide Can you please open a new issue for this? and add details and logs as possible to quickly solve it.

@ttytm
Copy link
Member

ttytm commented Jun 2, 2024

I created a simple app for the purpose of having an example. It uses Go-WebUI and creates its frontend with Bun and Svelte 5. Feature- and design-wise, it is an infant, but the focus is on providing a simple example that is hopefully easy to understand eventually.

gsysmo

https://github.com/ttytm/gsysmo

I'm planning to keep one branch simple, without complicating things, just adding some concise notes that might help to point out some concepts on how this stack works together for the app. The goal is to provide a simple way to what is needed for a fully bundled executable. Another branch is planned to add some real features to the application, which will be beyond an example

@jinzhongjia
Copy link
Contributor

jinzhongjia commented Jun 2, 2024

great, just to digress, I'd like to know if there are any system tray libraries I can use. @ttytm

@ttytm
Copy link
Member

ttytm commented Jun 2, 2024

A C99 lib I know of is: https://github.com/zserge/tray/tree/8dd1358b92562faf7c032cf5362fa97cbc7e13e9

One drawback is that the last commit was more than 6 years ago. MacOS arm, for example, is not fully supported. However, if it is not a very complex tray that you plan to have, writing it from scratch could work, the platform apis are more or less straight forward.

@laoshaw
Copy link

laoshaw commented Jun 2, 2024

also to digress, for webui with go in particular, it could be using a built-in go https library and websocket support(instead of civetweb which is C), so you can get cross platform webui all in one binary without using webview libraries like what Wails does. on the other hand, compiling civetweb is not that difficult either as what webui does

@ttytm
Copy link
Member

ttytm commented Jun 2, 2024

It will be an all-in-one binary using webui with civetweb.
I'd like to have it as an example that is fairly general, so that the concept of combining a framework with webui is usable with other webui wrappers.

The caveat for some languages will be embedding the UI files, not the websocket. When free moments open up I'll gonna add the embedding and also a deployment workflow to show how to achieve a single-binary output in a CI pipeline.

@neroist
Copy link
Member Author

neroist commented Jun 2, 2024

A C99 lib I know of is: https://github.com/zserge/tray/tree/8dd1358b92562faf7c032cf5362fa97cbc7e13e9

One drawback is that the last commit was more than 6 years ago. MacOS arm, for example, is not fully supported. However, if it is not a very complex tray that you plan to have, writing it from scratch could work, the platform apis are more or less straight forward.

There is a fork which is actively maintained: https://github.com/dmikushin/tray

@ttytm
Copy link
Member

ttytm commented Jun 2, 2024

Oh nice. That's actually really good to know thanks for sharing!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
documentation Improvements or additions to documentation
Projects
None yet
Development

No branches or pull requests

9 participants