danieldietrich / candid Goto Github PK
View Code? Open in Web Editor NEWCandid is a surprisingly fresh and frameworkless JavaScript library for building web applications.
License: MIT License
Candid is a surprisingly fresh and frameworkless JavaScript library for building web applications.
License: MIT License
The current custom class definition works find (at least in Chrome and Safari). Do we additionally need to define the constructor explicitly?
CustomElement.prototype.constructor = CustomElement.prototype.constructor`
We should test all scenarios, including new()
, document.createElement('my-element')
and maybe even Reflect.construct()
.
Motivation: the user must not think about
Status quo:
this.root = this.element
+ this[__ctx]
this.root = this.element.shadowRoot
+ this[__ctx]
Goal:
this[__ctx]
with this
=> the previous this.element
is now this
this.root
. if the user uses mode='open'
or mode='closed'
, then this.shadowRoot
is the root, otherwise this
(the user needs to take care of it, the default is no shadow root, so it should be transparent)I don't think we need to expose reflective information about the web component in the context object, because most information is already available and the use cases are rare:
this.tagName.toLowerCase()
(custom element) or this.getAttribute('is')
(customized built-in element)this.getAttribute('is') ? this.getAttribute('is') : undefined
this instanceof ...
and customElements.get(name)
(constructor)mode = this.shadowRoot?.mode
Core issue: create a custom element which is a subclass of a native DOM element using the ES5 prototype chain. Current problem is the instantiation using new().
Looks like lit.dev does not support it. One more reason for us to do it!
https://dev.to/lkraav/comment/ad06
See caniuse.com.
As of 2022-02-02
<!-- this should be on top of your HTML <head> scripts -->
<!-- it fixes customized built-in element in Safari and older browsers -->
<script src="//cdn.jsdelivr.net/npm/@ungap/custom-elements"></script>
Update: we need to distinguish this issue into two tasks:
type Options = {
mode?: 'open' | 'closed' | null // shadow root mode (default: null)
extends?: string | null // super tag name (default: null)
props?: object | null // observed properties and default values
template?: HTMLTemplateElement | null // the html template
}
// Candid.createWebComponent()
function createWebComponent(name: string, options?: Options)
Question: Should createWebComponent()
call customElements.define()
?
Currently we use a declarative approach only and only have one public API candid(options)
to initialize Candid.
We should think about exposing the JS API in order to allow to create web-components dynamically using JS. Maybe it also makes sense to expose the web-import API.
It is not sufficient to just export existing functions. Most probably we might need to re-design the internal API.
There are already many edge cases (and their combinations) to be tested
document.createElement('my-elem')
+ adding attributes and properties before connecting the element to the live DOMWe once had element validation and removed it. I think it is a good idea to re-add it, even if the messages/strings blow up the library.
Currently we define default values. It would be better to define types.
props: {
selected: Boolean,
index: Number
}
One challenge is to evaluate the script before defining the custom class because the prop names are needed for these static observedAttributes
method.
Background: Candid internally uses eval
to execute web component scripts.
See also #10
See https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP
If the web-component has mode="open"
or mode="closed"
then the root
is of type ShadowRoot
.
const { root } = this.ctx;
// = true
root instanceof ShadowRoot
// = false
root === this
If the web-component has no mode set, then the root
is the element.
const { root } = this.ctx;
// = true
root instanceof HTMLElement
// = true
root === this
Names:
candid-cli
candid
Commands:
init
- initialize a full fledged web projectbundle
- bundle web imports in order to reduce number of fetchesAnd bundle typings.
This feature is triggered by Candid's <web-component>
tag. Currently we have to define the whole code of each component within one big index.html
file. Without doubt, that was the goal of this library, having just HTML instead of build tools. However, it would be nice to be able to organize the markup (HTML tags, CSS <style>
, JS <script>
etc.) into smaller portions that are either loaded eagerly on page load or lazily on web component instantiation.
We will introduce a tag <web-import>
which works like this:
href
attribute points to a relative or an absolute URL containing arbitrary content<web-import>
, it will additionally set the status
attribute. The initial status is loading
. Eventually it will be either ok
or error
. If the state is ok
, the web component's innerHTML
will be updated with the fetched result. If the state is error
, the innerText will be updated with the error message.<web-import>
elements are processed. Those within <template>
tags are skipped.<web-import>
tags of the <template>
's content are processed if their state isn't set, yet.<web-import>
result has been loaded<web-imports>
's of a web-component's template have failed to load or errored, the web-component should not be upgraded.Currently Candid uses eval to execute <script>
code. Some pages use [CSP script-source](See https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/script-src) to disallow eval from other sources.
Content-Security-Policy: script-src 'unsafe-eval';
<template is="web-component">
<!-- definition -->
</template>
<template>
is not rendered<web-component>
<template>
<!-- definition -->
</template>
</web-component>
<style>web-component { display: none !important; }</style>
My feeling is that we should go back to <web-component>
because it is supported by all browsers.
Candid is unopinionated. We do not include polyfills or internally add additional styles to hide elements. It is the decision of the user to do so.
All scripts run once on initialization. During that time, this
is writable.
After initialization, this
is sealed (from the viewpoint of a web-component's <script>
).
I like the declarative rendering in React. It is based on virtual DOM diff. Web components have the attributeChangedCallback: each attribute change will lead to a call. It leads to code that looks like a big switch statement, each case directly mutates the DOM.
I want to think in directions, which lead more in direction of what React is doing, without being too rigid about how devs use the web APIs.
JS is single threaded. That means, Candid could 'record' one changes, by enqueueing an onUpdate
call, while writing subsequent attribute changes (which are performed by the current "thread") to one object. That way, the onUpdate
function of the user-space would be called only once and receive a complete object of changed properties instead of being called multiple times, receiving oldValue, newValue
tuples.
The main benefit I see is that the component logic could be more complex. We would still need to modify the DOM directly (in contrast to React, which diffs virtual DOMs), however, we could take the whole property object into account when rendering changes. That would be more efficient than processing subsequent property-changes.
Currently, the component processor only reads <script>
contents. It would be nice to by able to load scripts dynamically. Maybe we can just leave the <script>
tags in the DOM (resp. as part of the template) and the browser does the job when cloning the template to the custom element root?
I see the following problems with that approach:
<script src="...">
cannot bind this
. we currently rely on that because a script sees the component's __ctx
object as this
. I don't see how this should work...src
does not point to the right resource anymore. we could fix that by rewriting relative urls with the right base-urlSee also #20
It does not make sense to keep child web components (that are children of a web component's template) because the row content is copied to the DOM, including the template.
We should hide tags. This can be either done with CSS:
web-component, web-import {
display: none !important;
}
Alternatively this can be done declaratively:
<web-component name="..." hide></web-component>
<web-import src="..." hide></web-component>
const makeCE = (superTag) => {
const superType = ...;
return class extends superType { ... }
};
see https://twitter.com/justinfagnani/status/1489978756303253518
Currently we recursively use window.querySelectorAll()
to find Candid elements. We can reduce that from O(n^2) to O(1) by making web-component a custom element that is processed by the browser.
Motivation: [Browser events] bubble up the DOM tree. These kind of eventing can be used to send synthetic events. However, I see the need for eventing between distinct branches of the DOM tree.
<head>
and <body>
I know that it is easy to set the title of an HTML page directly. This is just an example of eventing between two unrelated elements.
<head>
<!-- listenes on the message bus for title changes -->
<title is"my-title">Waiting for a nav link click...</title>
</head>
<body>
<nav>
<ol>
<!-- a click sends an event over a message bus with a title -->
<li><a is="my-a" href="#">Flip</a></li>
<li><a is="my-a" href="#">Flap</a></li>
</ol>
</nav>
<web-import src="./my-components.html"></web-import>
<script type="module" src="//esm.run/candid"></script>
</body>
The eventing I described above follows the publisher subscriber (pub-sub) pattern. It is the simples form for general purpose eventing I can imagine.
Here is an example where it does not make sense so much to use the pub-sub pattern for communication. In particular, we have a my-form type aggregating my-input elements. If we have multiple instances of the form at the same time, keydown events of one input are received by every form that is currently mounted to the DOM. This is a good example, where the native event bubbling would make more sense. Each form wants to receive the (synthetic) events of its children.
<body>
<form is="my-form">
<input is="my-input"></input>
</form>
<form is="my-form">
<input is="my-input"></input>
</form>
</body>
<web-component name="my-form" extends="form">
<template>
<script>
const onKeyDown = (e) => { console.log('key down', e) };
this.onMount = () => { this.subscribe('my-keydown', onKeyDown) }
this.onUnmount = () => { this.unsubscribe('my-keydown', onKeyDown) }
</script>
</template>
</web-component>
<web-component name="my-input" extends="input">
<template>
<script>
this.element.onKeyDown(e => this.send('my-keydown', e));
</script>
</template>
</web-component>
Of course there are multiple solutions for this problem of receiving too many messages. One would be to add the form id or name to the message object and filter the messages by the form id. For now I would not provide a special filter API. All the (business) logic should be located in the <script>
tags of the web-component.
Candid would maintain three internal functions (modulo naming):
subscribe: <T>(topic: string, subscriber: (e: T) => void) => (e: T) => void
unsubscribe: <T>(topic: string, subscriber: (e: T) => void) => boolean
send: (topic: string, message: any) => void
that are exposed in each context object of a web component:
<web-component name="foo-bar">
<template>
<script>
// `this` points to the context object of this web component
this.subscribe('user-defined-event', (e) => {});
this.unsubscribe('user-defined-event', (e) => {});
this.send('user-defined-event', arbitraryObjectOrEvent);
</script>
</template>
</web-component>
{ display: none }
on construction{ display: none }
on constructionLet's target less than 1.5 KB brotlied. Some internal identifiers are still clear-text for example, that can be optimized by maintaining a whitelist of identifiers that shall not be minified. Preact (Microbundle) is a example for doing so. However, we should stick to the default build tools offered by Vite.
See spec
attachInternals()
formAssociatedCallback
, formDisabledCallback
, formResetCallback
and formStateRestoreCallback
We use promises in order to process lazily loaded resources. I suggest to catch errors on the outer-most layer and report them using
console.error('[candid] An error occurred.', error)
<head>
<link rel="preload" href="./my-components.html" as="fetch">
</head>
<body>
<web-import src="./my-components.html"></web-import>
</body>
If a web component is loaded lazily using <web-import>
for example, then there might be a style post-processor needed like TailwindCSS.
This can be achieved by passing a processor aka effect during initialization to Candid. When the DOM changes (e.g. because new web components are imported) then the effect is applied to the new nodes.
Candid itself also returns an effect that can be applied by other libraries when DOM nodes change (are added).
{
- root: HTMLElement | ShadowRoot
+ shadowRoot?: ShadowRoot
}
How does Turbo load remote code and replace the DOM? Are there any opportunities for us to reduce FOUC?
<body>
<!-- best practice: use a web component as root for the content in order to reduce FOUC -->
<!-- also: don't use slots to prevent FOUC -->
<app-content></app-content>
<!-- the template content is processed before connecting it to the live DOM -->
<script type="module">
import candid from "./src/index.js";
window.addEventListener('load', () => candid());
</script>
</body>
Before an element is upgraded you can target it in CSS using the :defined pseudo-class. This is useful for pre-styling a component. For example, you may wish to prevent layout or other visual FOUC by hiding undefined components and fading them in when they become defined.
Note: this does not help for customized built-in elements <div is="ext-div">Loading...</div>
Example: hide <app-drawer>
before it's defined:
app-drawer:not(:defined) {
/* Pre-style, give layout, replicate app-drawer's eventual styles, etc. */
display: inline-block;
height: 100vh;
opacity: 0;
transition: opacity 0.3s ease-in-out;
}
After <app-drawer>
becomes defined, the selector (app-drawer:not(:defined)
) no longer matches.
Source: google web fundamentals
Update:
bind script evaluation to
this
instead ofthis.ctx
. also: call all lifecycle methods withthis
instead ofthis.ctx
. can we just callonMount()
instead ofonMount.call(this)
to simplify the code?
I am not sure about this. The more I think about it, this.ctx
should be the this
of all web-component scripts because I don't want to risk to collide with the HTMLElement API. We should don't touch it.
freeze
this.ctx
after creation
I think it is a good idea to freeze the ctx object after the script ran. This is what is known as open-closed priciple in oop: should be open for extension, but closed for modification
idea: maybe we can 'hide' the ready state in
const ready = Object.isFrozen(this.ctx)
and completely remove the ready attribute
yes by all means
Currently each web-component instance binds this.ctx
to the scripts:
<web-component name="foo-bar">
<template>
<script>
this.element; // = the HTML element of this web-component
const { root, subscribe, unsubscribe, send } = this; // = the context object
</script>
</template>
</web-component>
I think it would be more natural to bind this
to the HTML element and have a separate context property ctx
:
<web-component name="foo-bar">
<template>
<script>
this; // = the HTML element of this web-component
const { root, subscribe, unsubscribe, send } = this.ctx; // = the context object
// additionally, this.ctx has writable onMount, onUnmount, onUpdate, onAdopt
</script>
</template>
</web-component>
this
instead of this.ctx
. also: call all lifecycle methods with this
instead of this.ctx
. can we just call onMount()
instead of onMount.call(this)
to simplify the code?this.ctx
after creationconst ready = Object.isFrozen(this.ctx)
and completely remove the ready attributeCandid is 100% pure web. It does not ship with an HTML Template engine or with compiler-supported variable bindings.
Such reactivity would look like this but it would be a first step to a framework that re-invents the wheel (known from React, Vue, Svelte and also not-so-well-known like Aurelia).
However, it would be good to have 3rd party template engines in mind, like Lit or even JSX.
But these may change over time. Relying only on vanilla HTML/JS is the most flexible and unopinionated approach. It is most important to get the API right (see #55):
this
(in a script)`Candid claims to be unopinionated but ships with its own messaging solution (pub-sub). It is better to move pub-sub to its own library.
export type Topic = string;
export type Subscriptions = Set<Subscriber<any>>;
export type Subscriber<T> = (e: T) => void;
export type Unsubscribe = () => void;
export default {
publish,
subscribe
};
const topics: Map<Topic, Subscriptions> = new Map();
/**
* Subscribes a subscriber to a certain topic.
* Returns an unsubscribe function.
*/
function subscribe<T>(topic: Topic, subscriber: Subscriber<T>): Unsubscribe {
let subscriptions = topics.get(topic);
if (!subscriptions) {
topics.set(topic, subscriptions = new Set());
}
subscriptions.add(subscriber);
return () => { subscriptions!.delete(subscriber) };
}
/**
* Publishes a message to all subscribers of a given topic.
* The subscribers are informed in no particular order.
*/
function publish(topic: Topic, message: any): void {
topics.get(topic)?.forEach(subscriber => {
try {
subscriber(message);
} catch (err) {
console.error("[pubsub] error publishing message:", err, "\n", { topic, message });
}
});
}
import { defineWebComponent } from 'candid'
const MyFoo = defineWebComponent(...)
const MyBar = defineCustomElement(...)
// export individual elements
export { MyFoo, MyBar }
export function register() {
customElements.define('my-foo', MyFoo)
customElements.define('my-bar', MyBar)
}
A declarative, efficient, and flexible JavaScript library for building user interfaces.
๐ Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. ๐๐๐
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google โค๏ธ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.