0kku / destiny Goto Github PK
View Code? Open in Web Editor NEWA reactive UI library for JavaScript and TypeScript
License: Open Software License 3.0
A reactive UI library for JavaScript and TypeScript
License: Open Software License 3.0
Maybe there should be a method for toggling boolean reactive values?
The old buggy version of slice
was reimplemented in a naive way that has unacceptable performance.
While the library works without a compiler, there are many optimization opportunities that would help apps perform better in production.
Optimization opportunities:
html` `;
template literals should be minified using HTML minification.ReactiveArray::get()
to avoid need for a Proxy. I.E. arr[0]
should be converted to arr.get(0)
, for example.ReactiveArray.splice()
to avoid need for a Proxy. I.E. arr[3] = "foo"
should be converted to arr.splice(3, 1, "foo");
, for example.ReactiveArray::splice()
. All modifications to an array are expressible with splice()
, and that's exactly what the class does internally. For most things, this would be a simple transformation, so inlining it would improve performance.destiny:out
, logic for that shouldn't be bundled in).It should conceivably be possible to type-check slots in HTML templates (html` `
). However, this would require an editor extension, or a plugin to TypeScript language services. I don't know what the most practical approach would be, or where to even get started. Needs investigation.
string
sprop:
) assign to a property on the DOM object of the element, so it should use the interface of the element in question. For example, with <input type=checkbox prop:checked=${foo}>
, foo
should be boolean
, because HTMLInputElement.checked
is boolean
.on:
) add event listeners using HTMLElement::addEventListener()
, so it should be type-checked accordingly. Example: <div on:click=${"hello!"}></div>
should error, because the event handler should be a function
, not a string
.call:
) call a method on the element in question. Ex: <form call:request-submit=${[]}></form>
calls HTMLFormElement::requestSubmit()
. It shouldn't allow it to be called incorrectly.destiny:
namespace specifies custom lib-specific behavior, that should be easy enough to type-check if the rest is implemented.For convenience, JSON-like structures are recursively converted to become reactive. However, TypeScript's type system can't tell plain objects from other ones. Support for nominal typing would help, but I wouldn't hold my breath on that ever getting implemented in TS. As a consequence, this is awful for DX, because objects unexpectedly breaking inside reactive structures.
Possible solutions:
Don't convert objects
This would be quite inconvenient in terms of DX, but might be the cleanest solution.
Flagging objects
One solution would be to make it mandatory to add a property to objects that acts as a "flag" for intent to be converted. This is almost as much work for developers as not converting objects at all, while being ugly.
Modify the original object
Conceivably, it should be possible to add a symbol property on objects, which would serve as a key for accessing reactive properties:
import {
reactive,
reactiveObjectSymbol, // is a Symbol; probably would need a more reasonable name
} from "destiny-ui";
const obj = new SomeObject;
const reactiveObj = reactive(obj);
console.log(obj === reactiveObj); // true: it adds the symbol property onto the original object, and returns it
console.log(obj[reactiveObjectSymbol]); // logs the reactive version of the input object
This seems to be the best of both worlds, even if it feels a bit dirty.
Gotten a few requests to support functional components, and after some deliberation, I think it might be possible to do that and it might improve developer ergonomics. Class-style components were the obvious choice because that's what needs to be used to write native Web Components, but I think it's possible to write a functional wrapper for the native Web Components. My primary concern is that I wouldn't want to create confusion by allowing two different ways of writing components, so if this new way was to be adopted, I would likely want to discontinue support for class components.
I think the example from the readme could look something like this:
import { html, disconnectedCallback } from "destiny";
function ExampleComponent (
props: {},
) {
const who = reactive("visitor");
const count = reactive(0);
const timer = setInterval(() => count.value++, 1e3);
disconnectedCallback(() => clearInterval(timer));
return html`
<label>What's your name? <input type="text" value=${who} /></label>
<p>
Hello, ${who}!
You arrived ${count} seconds ago.
</p>
`;
}
Each of the web component life cycles would get their own similarly named function exported by the library. Calling these functions outside a functional component would throw. They would not need to have the same limitations as React's hooks: the function's body is only executed once (during element construction), so calling these lifecycle methods conditionally would be fine. Calling these lifecycle methods registers them as callbacks for the respective lifecycle events on the component. The components would need to have a PascalCase name so the template preparser can tell them apart from callback props. The components would still be converted to native Web Components under the hood: the imported lifecycle hooks would be used to convert these functional components into classes for registration.
Unlike class components, these functional components would not extend HTMLElement
(obviously), which means that they could not be used by any methods that expect an element constructor as input, such as customElements.define()
, and would need to be registered by a method provided by the library if one wanted to register them. And since they wouldn't be classes, you also couldn't use instanceof
on them and you couldn't extend components. I find these limitations to be unimportant, but I'm open to counterarguments.
Currently, array methods that use external reactive entities are not recomputed when those get updated. Some methods offer to take an array of dependencies to explicitly declare what reactive entities the callback depends on, but this is error-prone, unintuitive, and easy to forget. They should instead work like computed()
does.
We should use Deno instead of Node for compiling so we can use Deno's nice features like deno doc
and deno test
.
Depends on denoland/deno#1739 denoland/deno#5253 and denoland/deno#3385
For ReactiveValue and ReactiveArray. See https://discord.com/channels/704026413870612511/704026414399225928/967977981706379344
This is just a list of things i've found that would be good to document, when you decide to start writing it. I may add more as i go along.
"What does register()
do? Whats the component name?" - From what i've seen, it's a wrapper around customElements.define
, and the component name, is the class name, lowercased, being separated by a -
before each capital letter, eg word. So for example register(class CButton ...)
produces a <c-button>
component
How to pass in complex data? - From the discord, this can be done using. <component prop:users=${[{...}, {...}]}
How to pass in handlers? - From the discord, i've seen this can be done by <component on:click=${handleClick}
Currently, creating new reactive values inside the callback of computed()
(and similar methods) is disallowed because accessing the value of a newly created reactive entity inside the computation would add that value to the dependencies of the computed value, creating a leak that piles on exponentially on every update.
The following example that fails was given:
const someRV = new ReactiveValue({foo: 1, bar: 2});
cosnt someOtherRV = new ReactiveValue("qux");
const elements = computed(() =>
Object.keys(someRV.value)
.map(key => html`
<p class=${someOtherRV}>${key}</p>
`)
);
The expected behaviour is, that the elements
array gets recomputed whenever someRV
changes. However, computed()
can't tell the difference between someRV
and someOtherRV
because the template synchronously accesses the value of someOtherRV
in order to build the document fragment, thus adding it to the dependencies of the computed value alongside someRV
. Thus, the entire array would be reconstructed whenever either of the two values change. While this works and is allowed, it unexpectedly does more work than intended and necessary, without warning you. Now, if one was to map someOtherRV
into a new reactive value, or use a computed value to create a new reactive value inside the template, this would throw, stating that creation of new reactive values inside a computed value is disallowed.
The correct way to write the above code would be moving the mapping outside the computation:
const elements = computedArray(() => Object.keys(someRV.value))
.map(key => html`
<p class=${someOtherRV}>${key}</p>
`);
This would work with the mapped or computed values inside the template too, since it would no longer be inside the computation. However, the question is, should the first example just work? Is making it work worth the added complexity?
Conceivably, this use-case could be supported by doing all of the following:
computed()
, computedArray()
, sideEffect()
, html
templates) into microtasks, deferring their evaluation until the previous computation is complete, thus allowing nested computations without one interfering with another.It's not clear if supporting the first pattern is worth the added complexity, considering that that specific use-case is enabled by changing the code to the second example. The code in the latter example is more idiomatic to the programming paradigm in question anyway, so I'm not sure if encouraging the former is wise. On the other hand, one might argue that it's surprising and unintuitive that it doesn't "just work", since in non-reactive code there wouldn't be any reason it would matter which order you do things in. Furthermore, perhaps there are additional use-cases I haven't thought of that can't so easily be refactored to side-step the issue? More feedback may be necessary.
reverse
and sort
don't make a whole lot of sense as mutable methods on ReactiveArray
s, and it would make more sense for them to return new arrays instead. With that in mind, I plan to add the following methods from the change Array by copy proposal:
withReversed
withSorted
…and remove reverse
and sort
. If someone needs the mutating version, they could simply do arr.value = arr.value.reverse()
for example.
Additionally, there are some other methods on native Arrays that don't make a whole lot of sense for ReactiveArrays with correct usage of the library, or are impossible to have their updates be optimized. With that in mind, I plan to remove flat
, flatMap
, copyWithin
, and fill
.
concat
is somewhat useful, but the semantics of native Array.prototype.concat()
are quite convoluted, and I would much rather have a simple function that does nothing but concatenate ReactiveArray
s. I don't know if it would make more sense to change concat
to have semantics that are not in line with the native equivalent, or to remove concat
and make a new function with simpler semantics to avoid confusion.
Finally, it seems like the mutating methods proposed and discussed in #4 haven't been very popular, and similar functionality could be achieved with similar effort using a for
loop, so I plan to remove the ones implemented: mutFilter
and mutMap
.
On ReadonlyReactiveArray:
bind
❇clone
❇concat
🚮?enties
every
exclusiveSome
❇filter
find
findIndex
flat
🚮flatMap
🚮forEach
get
includes
indexOf
join
keys
lastIndexOf
map
pipe
❇reduce
reduceRight
slice
some
toJSON
unbind
❇update
❇values
withReversed
🆕withSorted
🆕On ReactiveArray:
copyWithin
🚮fill
🚮mutFilter
❇🚮mutMap
❇🚮pop
push
reverse
🚮set
❇shift
sort
🚮splice
unshift
🆕 = proposed addition
❇ = not present on native Arrays
🚮 = planned for removal
Thoughts?
Current implementation is naive.
When a reactive value is assigned to another reactive value, or returned from the callback of computed()
, RV::truthy()
, RV::falsy()
, RV::pipe()
etc., it should probably be flattened for convenience, similar to how promises flatten.
To avoid confusion. As you know it's the exact same as xml
, but can and will cause confusion, especially amongst new developers.
Alongside this, what you pass into html
isn't strictly html
Unfortunately, @BobobUnicorn's PR to TS got closed, however the TS team said they're open to revisiting it at a later date. As such, we need to figure something else out to get type checking for templates. After exploring our options, it seems that just maintaining our own fork of TS seems like the least unreasonable approach for now. The goal with this move is to explore the viability of implementing an XML parser in TS types. The primary concern is performance. We're hoping to get the type checking performance with this approach to be acceptable, which would hopefully also convince the TS team that this is not a terrible thing to merge into the language officially.
A way of defining CSS that pierces shadow roots of components. I'm thinking of injecting a common stylesheet during component instantiation.
@b-fuze is working on a DOM API for Deno, which this feature depends on. The intention is to do SSR primarily using Deno because of its closer-to-web JS API. While I'm not super interested in adding support for SSR using Node, that's probably achievable with JSDOM; though the performance won't be up-to-par with the Deno version.
The current implementation of [Symbol.asyncIterator]()
on ReactiveArray
and ReactivePrimitive
is very sketchy. However, this doesn't seem to be practically fixable until browsers support ReadableStream::getIterator()
:
// for ReactiveArray
async *[Symbol.asyncIterator]() {
yield* new ReadableStream<[number, number, ...IArrayValueType<InputType>[]]>({
start: controller => this.#callbacks.add(
(
...args: [number, number, ...IArrayValueType<InputType>[]]
) => controller.enqueue(args)
),
}).getIterator();
}
Depends on:
Building project fails on TypeScript 3.9 and up. Culprit is ReactiveArray#flat().
Workaround (courtesy of @b-fuze):
const newArr = new this.species(...this.#value.flat(<1> <unknown> depth));
this.#callbacks.add(() => {
newArr.value = this.#value.flat(<1> <unknown> depth);
});
Let's keep using 3.8 for now, but resort to using the workaround if we need features from newer versions of TS before the compiler bug is fixed.
Depends on microsoft/TypeScript#38298
There's a memory leak in the current build, and I don't know what's causing it. Needs investigation.
By design, ReactiveArray
mirrors the functionality of native Array
s as closely as feasible. The major difference is, that instead of returning plain values and new Array
s, it returns ReactivePrimitive
s and new ReactiveArray
s. For example, ReactiveArray.length
is Readonly<ReactivePrimitive<number>>
instead of number
.
The library encourages mutability, because that's how it tracks changes to changes to things like arrays. However, some methods on native Array are immutable, which in some cases orthogonal to the workflow in Destiny UI. With this in mind, ReactiveArray specifies some mutable alternatives to the immutable ones, such as ReactiveArray::mutFilter()
and ReactiveArray::mutMap()
which are equivalent to Array::filter()
and Array::map()
respectively, except that they mutate the array, instead of creating a new one. ReactiveArray::filter()
and ReactiveArray::map()
are naturally also available, so the mutable versions are an additional addition. The immutable versions return a piped readonly ReactiveArray, which is kept updated with the original array.
Do these additional mutable versions provide meaningful value to warrant increased bundle size? Are they confusing? Should all immutable array methods that return a new array have a mutable equivalent?
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.