Skip to content

Ultra minimalistic UI/Web components and Chrome extension Javascript framework. (OMG not another one!)

License

Notifications You must be signed in to change notification settings

izo0x90/kiss-components.js

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

9 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

đź’‹ kiss-components.js

Ultra minimalistic UI/Web components and Chrome extension Javascript framework. (OMG not another one!)

There are two parts to this "framework" that are related but can be used independantly from eachother:

  • KISS-WebUI, This is a minimal WebComponent framework that provides just enough scaffold to define a UI App and Components
  • KISS-Chrome-extension, This is a minimal library (yes, this one is just a library) to help with writing Chrome extensions

And I suppose there is third part to all of this:

  • Set of boilerplate templates that go along with the peices mentioned above, as a way to setup projects quickly and define the intended approach

WebUI Overview

  • Structure to define UI web apps
  • A simplified way to define WebComponents
  • Basic utils to work with WebComponent HTML templates and shadowdom
  • Opinionated approach to interaction between UI app and "business" logic
    • Using "public" events and "public" UI app commands

Chrome-extension Overview

  • A message dispatching and command registration system to allow interactions between the extension and the content sctipts (where the actual website code/ data reside)
  • A way to easily inject content scripts in the site and use code sparated in ECMA modules

WebUI Quick start

Start by creating root UI app component as seen in the template below. All componenets inherit from the BaseComponent class follow the structure shown in the web_ui templates, this is what takes care of handling all the WebComponents boilerplate.

import { BaseElement, PublicUiEvent } from 'path_to_library/kiss/kiss_web_ui/base.js'
import { ExampleComponent } from './component_template.js'
export class ExamplePublicEvent extends PublicUiEvent {
static EVENTNAME = 'exampleEvent'
static DataClass = class {
constructor (eventDetailsPropInput) {
this.someEventProp = eventDetailsPropInput
}
}
constructor (eventDetails) {
const eventData = { detail: eventDetails }
super(ExamplePublicEvent.EVENTNAME, eventData)
}
}
export class UiApp extends BaseElement {
template = TEMPLATESTRING
#PROPNAME = 'some-prop'
#EXAMPLECOMPONENTID = 'example-component'
buildComponent () {
const templateInstance = this.defaultTemplate
const exampleChildComponent = new ExampleComponent()
exampleChildComponent.id = this.#EXAMPLECOMPONENTID
templateInstance.utils.appendById('components-container', exampleChildComponent)
return templateInstance
}
static get observedAttributes () {
return []
}
onMount () {
console.log('APP Mounted!')
// Register event handlers here
this.domUtils.addListenerById('refresh-button', 'click', this.eventHandlers.refreshClick)
}
// Organize event handler methods under .eventHandlers
eventHandlers = {
refreshClick: (_) => {
const someEventProp = this.domUtils.byId(
'main-element'
).getAttribute(this.#PROPNAME)
this.commands.requestResults(someEventProp)
}
}
// Publicly available commands are under .commands only on the root app component
// the root component is responsible for managing the rest of the child components
commands = {
requestResults: (someEventProp) => {
this.dispatchEvent(new ExamplePublicEvent(new ExamplePublicEvent.DataClass(someEventProp)))
},
updateResults: (resultsData) => {
this.domUtils.byId(this.#EXAMPLECOMPONENTID).replaceResults(resultsData)
}
}
}
const TEMPLATESTRING = `
<div id="main-element" some-prop="true">
<button id="refresh-button">Click me!</button>
<div id="components-container">
</div>
</div>
`

The creating and hydrating the Components DOM is done in the componenets buildComponent method. Here we get a new template instance from this.defaultTemplate and are then able to manipulate the template instance DOM using the provided DOM utils under templateInstance.utils. This template instance is returned by the buildComponent method and becomes the components shadow DOM.

buildComponent () {
const templateInstance = this.defaultTemplate
const exampleChildComponent = new ExampleComponent()
exampleChildComponent.id = this.#EXAMPLECOMPONENTID
templateInstance.utils.appendById('components-container', exampleChildComponent)
return templateInstance
}

Components shadow DOM can be manipulated to up dynamically update the component by using the utilities on the component instance under this.domUtils.

onMount () {
console.log('APP Mounted!')
// Register event handlers here
this.domUtils.addListenerById('refresh-button', 'click', this.eventHandlers.refreshClick)
}

On the root componenet is where we declare any "public" commands that the business logic will use to request change in the UI. In the case of the template file here we are also declaring our "public" events that the UI will issue when it needs to communicate something to the business logic. You can see how the business logic, instanciates the UI app, subscribes to those public events, and calls UI commands in the template below.

import { initSimpleWebCApp } from 'path_to_library/kiss/kiss_web_ui/utils.js'
// Initialize
const { appUi, publicUiEvents } = await initSimpleWebCApp('/path_to_app_components_code', 'example-extension-app-id')
async function refreshResults (eventDetails, callback) {
console.log('Do some required processing, I/O etc. and call the appropriate UI app command')
// Pass the callback to some async function/s that is doing the processing
// Pretend async block
const pretendData = 'DATA'
callback(pretendData)
}
// Register UI event handlers
document.addEventListener(
publicUiEvents.ExamplePublicEvent.EVENTNAME, (event) => {
console.log('Example event, details:', event.detail)
if (event.detail.someEventProp != null) {
refreshResults(
event.detail,
appUi.commands.updateResults
)
}
}
)

Chrome-extension Quick start

The chrome extension libarary allows to inject a contnet script in the webpage and have the extension execute commands that have been define in the content scripts by using a command channel that is established between both. This allows the extension to manipulate the underlying web page, extract data from its DOM, issue fetch request in the pages/tabs context etc.

Here you can see how the extensions script is able to issue a command to execute in the active tab and get back the results of that command:

import { CommandChannel } from 'path_to_library/kiss/kiss_chrome_extension/message_utils.js'
import { COMMANDS } from './commands.js'
// Chrome extension specific
const commandMessageChannel = new CommandChannel()
// State management
function executeTestCommandInContentScript () {
const payloadToSendTestCommand = {someData: 'Test data'}
commandMessageChannel.sendCommandToActiveTab(COMMANDS.TEST, (results) => {
// Here we can execute any callback once the Test command has executed and its results are returned
console.log('Returning fresh results!')
}, payloadToSendTestCommand)
}

And below is the basic way to set up those commands in the content script module:

// This is the main content script module that will get injected into the page with access to it's DOM etc.
import { CommandChannel } from 'path_to_library/kiss/kiss_chrome_extension/message_utils.js'
import { COMMANDS } from './commands.js'
// Commands
async function testCommand (inputData) {
// This code will execute in the webpages sandbox, allowing to access it's contents, issue fetch requests etc.
console.log(`TEST COMMAND REQUEST RECEIVED, Got ${inputData}`)
const results = document.getElementsByTagName('h3')[0]?.textContent
return new Promise(resolve => resolve(results))
}
// The chrome extension script can trigger commands to execute in the webpages sandbox by sending a
// message along the commandMessageChannel, as demonstrated in `popup_script.js`.
// Results of a command will be sent back to the caller along the channel.
// Event listeners and messages
const commandsMap = {
[COMMANDS.TEST]: testCommand
}
const commandMessageChannel = new CommandChannel(commandsMap)
commandMessageChannel.listenForCommands()
// Init finished
console.log('Content script injected into web page.')

The main content script module will be automatically injected in the page, allowing for ECMA style modules loading for any additioal code that needs to run in the page/ tab sandbox. The library finds the the main content script module by looking for the first file with name main.js listed under the first webresource listed in manifest.json, so make sure set that up accordingly:

"web_accessible_resources": [
{
"matches": ["*://*.desired.domain.here/*"],
"resources": [
"main.js",
"path_to_library/kiss/kiss_chrome_extension/message_utils.js"
]
}
]

For more complete details look here:

https://github.com/izo0x90/kiss-components.js/blob/632dbcef2c082332a8bcdd66ad8fcdc3d6ebd3bb/boilerplate_templates/chrome_extension/

KISS WebUI and Chrome extension used together

Checkout:

https://github.com/izo0x90/kiss-components.js/tree/632dbcef2c082332a8bcdd66ad8fcdc3d6ebd3bb/example

and

https://github.com/izo0x90/kiss-components.js/tree/632dbcef2c082332a8bcdd66ad8fcdc3d6ebd3bb/manifest.json