GithubHelp home page GithubHelp logo

0kku / destiny Goto Github PK

View Code? Open in Web Editor NEW
53.0 53.0 6.0 571 KB

A reactive UI library for JavaScript and TypeScript

License: Open Software License 3.0

HTML 0.30% TypeScript 95.48% JavaScript 4.22%
framework frontend javascript reactive-programming typescript ui

destiny's People

Contributors

0kku avatar bobobunicorn avatar dependabot[bot] avatar thatsnomoon avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar

destiny's Issues

Some Notes For Documentation

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.

  1. "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

  2. How to pass in complex data? - From the discord, this can be done using. <component prop:users=${[{...}, {...}]}

  3. How to pass in handlers? - From the discord, i've seen this can be done by <component on:click=${handleClick}

Investigate array methods being computed

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.

Implicit recursive conversion of objects

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:

  1. Don't convert objects
    This would be quite inconvenient in terms of DX, but might be the cleanest solution.

  2. 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.

  3. 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.

Needs editor tooling

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.

  • Normal attributes (no namespace) should be strings
  • Properties (prop:) 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.
  • Event listeneres (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.
  • Method calls (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.

Needs an optional compiler

While the library works without a compiler, there are many optimization opportunities that would help apps perform better in production.

Optimization opportunities:

  • Code inside html` `; template literals should be minified using HTML minification.
  • All dynamic property accesses using bracket notation should be converted to ReactiveArray::get() to avoid need for a Proxy. I.E. arr[0] should be converted to arr.get(0), for example.
  • All dynamic property assignments using bracket notation should be converted to ReactiveArray.splice() to avoid need for a Proxy. I.E. arr[3] = "foo" should be converted to arr.splice(3, 1, "foo");, for example.
  • All mutating ReactiveArray methods should be converted to 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.
  • Slotless HTML templates could be converted to a variant that doesn't go through the trouble of parsing the HTML at all
  • It should be able to aggressively tree-shake the library to get rid of class methods and other features that are not used by the app being compiled. Also, removing unused attribute namespace logic would be nice too (ex. if no elements in the app use destiny:out, logic for that shouldn't be bundled in).

Memory leak

There's a memory leak in the current build, and I don't know what's causing it. Needs investigation.

Implementation of @@asyncIterator is bad

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:

Investigate easing restrictions on computed()

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:

  • Making computations (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.
  • Allowing creation of reactive entities inside computed values by performing the following steps:
    1. Make an AbortSignal available when a computation starts
    2. Have any reactive entities that are created during the computation make a note of the currently running computation and its abort signal
    3. Have any reactive entities that were created refrain from adding themselves as a dependency to the currently running computation
    4. Have the computation abort the signal once the computation is done
    5. In response to the signal being aborted, have any reactive entities that were created remove the note about the computation, releasing it for use in future computations.

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.

Investigate functional components

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.

Type checking for templates

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.

ReactiveValues should probably flatten

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.

SSR

@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.

Changes to ReactiveArray

reverse and sort don't make a whole lot of sense as mutable methods on ReactiveArrays, 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
  • are the others in the linked proposal needed? Are there some other methods that should be added?

โ€ฆ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 ReactiveArrays. 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.

Click for a list of `ReactiveArray` methods

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?

Support global CSS

A way of defining CSS that pierces shadow roots of components. I'm thinking of injecting a common stylesheet during component instantiation.

Build fails on TS3.9+

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

Custom mutable methods on ReactiveArray

By design, ReactiveArray mirrors the functionality of native Arrays as closely as feasible. The major difference is, that instead of returning plain values and new Arrays, it returns ReactivePrimitives and new ReactiveArrays. 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?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.