rollthecloudinc / quell

Climate aware CMS breaking web apps free from carbon emissions.
https://demo.carbonfreed.app/pages/create-panel-page
GNU General Public License v3.0
14 stars 1 forks source link

Public API Bridge #18

Open verti-go opened 3 years ago

verti-go commented 3 years ago

custom javascript/typescript -> bridge -> angular app custom javascript/typescript <- bridge <- angular app (idea)

The goal here is to allow external scripts to be included at runtime that can alter the main application. The secondary goal is to make that api extensible. This will be done through the plugin system.

The intent of the public api bridge is to allow external scripts to communicate with the main application through a limited, safe, secure interface. An interface that will be extensible through the plugin system. This will allow external scripts to be included at runtime that can interact with the main application.

Panes will have user customizable state. The api bridge will allow a panes state to be changed using custom javascript or typescript code. Those state changes will be integrated with the context system. Similar to when a path variable or query string param changes trigger a contextual change that can be hooked into to alter the panel page display.

When this is complete panes will allow js and typescript that can directly communicate, change behavior of the main app. This includes changing the public state of a pane that panel pages can hook into to change behavior via the context system.

document.getElementsByTagName('div')[0].addEventListener('click', () => {
  context.displayAssociatedPane = !context.displayAssociatedPane;
});

In this example when the first div in the pane is clicked the displayAssociatedPane context value is toggled. This triggers a context change that conceptually would hide and display another pane. The pane would be hidden and displayed using rules associated with the pane. Pane rules already integrate with contexts. The context system is the fundamental building block that makes this possible. Panel pages have a tight integration with contexts to control rendering through an extendable application system architecture.


Original Problem:

Buttons or links that can change state triggering contextual triggers. This could be used to display different pieces of content based on those states. For example, clicking on an item that says "app.js" and displaying the panel associated with that rule without altering the url.

verti-go commented 3 years ago

Maybe a generic behavior overlay.

The overlay would allow selection of pane elements. Associating those selections with pluggable behavior selection events. When triggered do something - what?

thoughts:

select using css selectors: .alert event: click handler: alert('hello')

Ok, so now we are talking about allowing allowing people to add, inject their own js/ts. That is another can of worms. Although it might prove useful.

It seems like this would mostly be applicable to snippet content.

I think this would need to be some type of module separate from panels, pages that manages selections, event bindings, and handlers. Each of these could be plugins defined in the module.

What we would probably be after is something like...

selection type - ie. css selector -> compatible with html event: all events compatible with content type (html) and selection. For example, a button a selection would have different event compatibility from a div.

It's kind of a like a rule system. When this selection:event is matched do this thing. right?

Would it be possible to push code handlers to s3. Execute a build process that can include those build handler scripts and make them part of the native compilation process for an individual application.

Now we are talking about a cotent type that can actually be native typescript code that integrates directly with the application.

A pane could be setup that uses native ts including event handlers.

<div (click)="sayHello()">Say Hello</div>
sayHello() {
  alert('Hello!!!');
}

A bridge could be created to support allowed interactions between user defined code and the running, main application.

Standard js can also just be supported

document.getElementsByTagName('div')[0].addEventListener('click', evt => {
  alert('Hello');
});

We can also handle this like css. Instead of including in compilation store in isolation as flat files on s3. Add the script dynamically. Isolate within its own context. Provide a bridge that allows the script to speak with the main app through a limited interface.

A separate context could exist for each pane. That context could be one of the things that can be changed via the bridge.

Panes compatible with system or content types compatible with this system could allow assignment of a context definition. The context definition would describe a data structure which the script binding can access and change. The change would then trigger the main applications context system.

document.getElementsByTagName('div')[0].addEventListener('click', evt => {
  context.displayAssociatedPane = !context.displayAssociatedPane;
});

Panel pages are already integrated with contexts. Latch onto the context system to support custom behavior changes within the application. Changes initiated by custom, user code or abstractions using a gui.

Would it make sense to turn the bridge into a web socket interaction?

There is no way to completely isolate the main app when including random scripts. If someone wants to mess around they can. However, the people building these pages will part of the orgs, business, etc. If not than this is a feature that should be cut off. Possibly replaced by a gui of some type that can better detect and manage security threats. Perhaps rewriting the code with certain restrictions. Like selecting content and using the bridge. Any detection of processes other than that become flagged.

verti-go commented 3 years ago

The technical mvp here would be something like.

associate js file with panel page add js file

Something wrong with that.

In order to get at the content that people would be concerned with requires applying scripts once the content is available.

If one were to apply this to snippet the js script would need to evaluated after rendering that snippet to the page. This can really only be detected within the pane associated with the content type. Also changes that are triggered on that pane will change the dom elements. Therefore, the script would need to be reapplied when these types of things occur. Something I also need to deal with for css.

verti-go commented 3 years ago

Architecture

page builder state includes user customizable state for each *compatible pane on the page. That state will need to contain a separate AttributeValue. Each attribute value will be mapped to a single pane. Context state bridge is nested hierarchy of objects including getters and setters for the pane context.

This should be decoupled as much as it can be from pages, panels.

I think we are going to need to create an object that replicates the page hierarchy. Where we have page:panels[]:panes[].

const state /* as page */ {
  panels: [
    {
      panes: [
        { state: new AttributeValue() },
        { state: new AttributeValue() }
      ]
    },
    {
      panes: [
        { state: new AttributeValue() }
      ]
    } 
  ]
}

This can also be made more generic using a map.

{
  [`panelpage:${id}__${panelIndex}__${paneIndex}`]: AttributeValue
}

I kind of like that.

State management can be isolated in a separate, generic module.

verti-go commented 3 years ago

So what we end up with is a base module for the bridge. The bridge exposes a plugin to register interfaces that become part of the public, callable api bridge. The bridge module is responsible for connecting the main application with external code. The bridge module wires that all up without registering any api itself.

Other modules are created that register plugins with the bridge to add functionality. Functionality that provides a gateway for a targeted, safe communication interface between external code and the main, Angular application.

external script -> bridge call -> bridge module -> bridge plugin selection -> delegate action to plugin

verti-go commented 3 years ago

How is a pane context defined? Where is it defined?

It should probably be defined in the script associated with the pane. That way users can write code that is typed to it.

const context = {
  displayAssociatedPane: false
};

document.getElementsByTagName('div')[0].addEventListener('click', () => {
  context.displayAssociatedPane = !context.displayAssociatedPane;
});

I need to be able to read the context definition to convert it into a rule. Unless that needs to be manually entered...

I would like to have a linked select list to choose a specific property for a rule match.

Do pane rules integrate with context like this. Need to check that out.

I think the pane rule system is broken right now.

The context constant would also need to be replaced with a new object that integrated with the bridge but doesn't require the user to know anything about the specific, implementation details of the bridge.

The context also can't be global because there could be several context defined. One context per compatible pane. So those refs need to be replaced with variables, wrapped, or something.

However, I do not want to require user to know about those things. I want it all handled automatically. All they need to to do is a write a script like above. Simple.

ng-druid commented 3 years ago

pane context integration with rules and datasources (rest)

ng-druid commented 3 years ago

Given a panel page with three panes. Each pane has a js script associated with it that defines a pane context. The user should be able to use the same variable "context" across all pane js files. Each pane js file will be added in the global space. If each file defines a context variable the last one loaded will override all the others. This is not what we want. Instead context is going to need to be some type of interface that delegates to the proper pane context.

ng-druid commented 3 years ago

Instead of making the bridge a class, dynamically build the bridge using plugin passes? Each plugin has a chance to alter the bridge adding its own functionality.

The state is dynamically created using

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty

ng-druid commented 3 years ago

pane.stateDefinition: Array

State structure described as attribute value hierarchy. Do we need to create a component that allows a user to describe the structure via the ui? || when page persisted fetch js file and read context into pane and save behind the scenes. No need to create ui. Everything self contained in js. Caveat here is that js file types not supported. So if we want types the structure eiher needs to be defined via a ui form or using json schema maybe. We could do both. Create component to build state / attribute value hierarch. Upload json schema and automate ui form state. We could aloso allow ts file to be used where context could than have type info used to build attributes.

ng-druid commented 3 years ago

Bridge has been created. Bridge can be extended using plugins. Example of extending bridge simple.

export const testBridgeFactory = () => {
    return new BridgeBuilderPlugin<string>({
        id: 'test',
        title: 'Test',
        build: () => {
            PublicApiBridgeService.prototype['sayHello2'] = () => {
                alert('say hello 2');
            };
        }
    });
};

Factories should be used to create bridge builder plugins. The builder is then registered like any other plugin inside the module constructor.

import { APP_INITIALIZER, NgModule } from '@angular/core';
import { bridgeAppInit, testBridgeFactory } from './bridge.factories';
import { BridgeBuilderService } from './services/bridge-builder.service';
import { BridgeBuilderPluginManager } from './services/bridge-builder-plugin-manager.service';

@NgModule({
  declarations: [],
  imports: [
  ],
  exports: [],
  providers: [
    { provide: APP_INITIALIZER, useFactory: bridgeAppInit, deps: [ BridgeBuilderService ], multi: true }
  ]
})
export class BridgeModule { 
  constructor(
    bmp: BridgeBuilderPluginManager
  ) {
    console.log('bridge constructor');
    bmp.register(testBridgeFactory());
  }
}

Bridge is extended with function to resolve current context value by name w/ optional data arg. Bridge is extended with function to change panel pane state.

Test file created. Linkage hard coded right now into panel page to load this external script from cdn.

// bridge.sayHello2();
bridge.resolveContext('page')
  .then(v => bridge.writePaneState({ id: v.id, panelIndex: 0, paneIndex: 0 }, { displayAssociatedPane: true }))
  .then(pps => {
    console.log('wrote panel state');
    console.log(pps);
  });
ng-druid commented 3 years ago

The next step is to hook this into context and rules. Once hooked into context and rules external scripts will be able to interact with panel pages via changing pane state through the bridge which will than trigger any behaviors bound to the state key.

ng-druid commented 3 years ago

Right now the api requires id, panel index and pane index to to change state. I think I can work arounb this using web components and loading the external script into the shadow root. At which point any variables in the script will be isolated to the shadow root. So I will be able declare id, panel index, and pane index as globals restricted to the shadow root. I can than create another bridge function that wraps the lower api that indirectly includes the id, panel index and pane index.

ng-druid commented 3 years ago
windbeneathyourwings commented 3 years ago

The public API bridge has been implemented as a stand alone feature that will be used to support all communication with external applications.