Giter VIP home page Giter VIP logo

million's Introduction

Million.js Banner

What is Million.js?

Million.js is an extremely fast and lightweight optimizing compiler that make components up to 70% faster.

Oh man... Another /virtual dom|javascript/gi framework? I'm fine with React already, why do I need this?

Million.js works with React and makes reconciliation faster. By using a fine-tuned, optimized virtual DOM, Million.js reduces the overhead of diffing (try it out here)

TL;DR: Imagine React components running at the speed of raw JavaScript.

Installation

The Million.js CLI will automatically install the package and configure your project for you.

npx million@latest

Once your down, just run your project and information should show up in your command line!

Having issues installing? โ†’ View the installation guide

Why Million.js?

To understand why to use Million.js, we need to understand how React updates interfaces. When an application's state or props change, React undergoes an update in two parts: rendering and reconciliation.

To show this, let's say this is our App:

function App() {
  const [count, setCount] = useState(0);
  const increment = () => setCount(count + 1);
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={increment}>Increment</button>
    </div>
  );
}

In this App, when I click on the button, the count state will update and the <p> tag will update to reflect the new value. Let's break this down.

Rendering

The first step is rendering. Rendering is the process of generating a snapshot of the current component. You can imagine it as simply "calling" the App function and storing the output in a variable. This is what the App snapshot would look like:

const snapshot = App();

// snapshot =
<div>
  <p>Count: 1</p>
  <button onClick={increment}>Increment</button>
</div>;

Reconciliation

In order to update the interface to reflect the new state, React needs to compare the previous snapshot to the new snapshot (called "diffing"). React's reconciler will go to each element in the previous snapshot and compare it to the new snapshot. If the element is the same, it will skip it. If the element is different, it will update it.

  • The <div> tag is the same, so it doesn't need to be updated. โœ…
    • The <p> tag is the same, so it doesn't needs to be updated. โœ…
      • The text inside the <p> tag is different, so it needs to be updated. โš  ๏ธ
    • The <button> tag is the same, so it doesn't need to be updated. โœ…
      • The onClick prop is the same, so it doesn't need to be updated. โœ…
      • The text inside the <button> tag is the same, so it doesn't need to be updated. โœ…

(total: 6 diff checks)

<div>
-  <p>Count: 0</p>
+  <p>Count: 1</p>
  <button onClick={increment}>Increment</button>
</div>

From here, we can see that the <p> tag needs to be updated. React will then update the <p> DOM node to reflect the new value.

<p>.innerHTML = `Count: ${count}`;

How Million.js makes this faster

React is slow.

The issue with React's reconciliation it becomes exponentially slower the more JSX elements you have. With this simple App, it only needs to diff a few elements. In a real world React app, you can easily have hundreds of elements, slowing down interface updates.

Million.js solves this by skipping the diffing step entirely and directly updating the DOM node.

Here is a conceptual example of how Million.js reconciler works:

function App() {
  const [count, setCount] = useState(0);
  const increment = () => setCount(count + 1);

  // generated by compiler
  if (count !== prevCount) {
    <p>.innerHTML = `Count: ${count}`;
  }

  <button>.onclick = increment;

  // ...
}

Notice how when the count is updated, Million.js will directly update the DOM node. Million.js turns React reconciliation from O(n) (linear time) to O(1) (constant time).

How fast is it? โ†’ View the benchmarks

Resources & Contributing Back

Looking for the docs? Check the documentation or the Contributing Guide out. We also recommend reading Virtual DOM: Back in Block to learn more about Million.js's internals.

Want to talk to the community? Hop in our Discord and share your ideas and what you've build with Million.js.

Find a bug? Head over to our issue tracker and we'll do our best to help. We love pull requests, too!

We expect all Million.js contributors to abide by the terms of our Code of Conduct.

โ†’ Start contributing on GitHub

Alt

Codebase

This repo is a "mono-repo" with modules. Million.js ships as one NPM package, but has first class modules for more complex, but important extensions. Each module has its own folder in the /packages directory.

You can also track our progress through our Roadmap.

Module Description
million The main Virtual DOM with all of Million.js's core.
react / react-server React compatibility for Million.js.
compiler The compiler for Million.js in React.
jsx-runtime A simple JSX runtime for Million.js core.
types Shared types between packages

Sponsors

Acknowledgments

Million.js takes heavy inspiration from the following projects:

License

Million.js is MIT-licensed open-source software by Aiden Bai and contributors:

million's People

Contributors

38elements avatar abhiprasad avatar aidenybai avatar aslemammad avatar cbbfcd avatar dependabot[bot] avatar destroyer22719 avatar drex72 avatar eltociear avatar eniodev avatar exuanbo avatar ftonato avatar haideralipunjabi avatar jasonappah avatar johnjyang avatar lalitkumawat1m avatar nisargio avatar nojiritakeshi avatar olaoluwaayanbola avatar oliverloops avatar poteboy avatar quiibz avatar rakshixh avatar roar022 avatar sukkaw avatar sukritmalpani avatar timonwa avatar tobysolutions avatar uzairnoman avatar willdoescode 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  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  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  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

million's Issues

Bug: `patch` does not remove `className` attribute from element

Describe the bug

When calling `patch` to swap a VNode with a className to one without a className, the className is not removed from the actual element.

To Reproduce

Codesandbox Reproduction

import { m, createElement, patch } from "million";

const withClass = m("div", { className: "blue-text" }, ["Hello"]);

const element = createElement(withClass);

document.body.appendChild(element);

const withoutClass = m("div", {}, ["World"]);

patch(element, withoutClass);

const withEmptyClass = m("div", { className: "" }, ["World"]);

patch(element, withEmptyClass);

At the end of this script, after a patch with no className prop and one with an empty string, blue-text will still be attached to the HTML element.

Expected behavior

At the end of the above script, the HTML would not have a `blue-text` class in it.

Screenshots

No response

Operating System

MacOS Monterey

Browser

Google Chrome 90

Additional context

No response

Bug during Jsx children with one child only

Discussed in https://github.com/aidenybai/million/discussions/149

Originally posted by adi-L November 2, 2021
I'm not certain where is the bug, and what the cause of the issue.

The Code:

const Example = (props)=> <p><span>Hello world</span></p>

The Error:
Uncaught TypeError: Found non-callable @@iterator

It works fine if there is more than one child.
like this code:

const Example = (props)=> <p><span>Hello</span><span>world</span></p>

the error happens at the file 'jsx-runtime.esm.js'
at line 92

return h(tag, props, ...children);

it works fine when I changed it to:

return h(tag, props, children);

without the spread operator.

million version: 0.6.8
typescript
full code example: https://github.com/adi-L/million-jsx

Bug: createElement doesn't support Fragment as root VNode.

Describe the bug

The return type of Fragment is VNode[].

const Fragment = (props?: VProps): VNode[] | undefined => <VNode[] | undefined>props?.children;

However, createElement doesn't support passed in an array of VNode:

export const createElement = (vnode: VNode, attachField = true): DOMNode => {
if (typeof vnode === 'string') return document.createTextNode(vnode);
const el = vnode.props?.ns

To Reproduce

import { createElement } from 'million';

function Example() {
  return (
    <>
      {
        ['A', 'B', 'C'].map(i => (<div>{i}</div>))
      }
    </>
  );
}

document.body.appendChild(createElement(Example));

Expected behavior

Render <div>A</div><div>B</div><div>C</div> instead of <undefined></undefined>.

Screenshots

No response

Operating System

Not applicable

Browser

Not applicable

Additional context

No response

bug: How to tranform raw HTML to VElement

Describe the bug
I am developing a markdown renderer with vdom. As we all know, highlight.js is used to help us to highlight the source code. So I want to use it in the renderer, I try to use the following codes to meet my goal:

import { html } from "million/html"

export const fromStringToVNode = (content: string): VNode[] => {
    const h = html`${content}`
    if (Array.isArray(h)) {
        return h
    }

    return [h]
}

But I have not got the correct VNode but HTML string

Or,

import { fromStringToDomNode, fromDomNodeToVNode } from "million/ssr"

export const fromStringToVNode = (content: string): VNode => {
  return fromDomNodeToVNode(fromStringToDomNode(content))
}

But I have got the error Uncaught (in promise) TypeError: Cannot read properties of null (reading '__m_old_vnode')

So I want to know about how to transform raw HTML string (Maybe with \n) to VElement?

To Reproduce
Steps to reproduce the behavior:

  1. import highlight.js
  2. transform any TypeScript codes via highlight.js
  3. input the codes before
  4. Run

Expected behavior
Return correct VElement array

Device (please complete the following information):

  • OS: [e.g. iOS] Windows 11
  • Browser [e.g. chrome, safari] Edge Chromium
  • Version [e.g. 22] Latest

Additional context
Add any other context about the problem here.

Uncaught SyntaxError: Cannot use import statement outside a module

Describe the bug
I wanted to use million.js but I got this error Uncaught SyntaxError: Cannot use import statement outside a module

To Reproduce
I installed million and this is my file:

import {m , patch , createElement} from "million"

const app = createElement(m("h1" , {id:"app"} , ["helloworld"]))
document.body.appendChild(app)
patch(app , m("div" , {id:"app"}, ["goodbye"]))

Screenshots
Screenshot from 2021-07-07 13-00-22

Desktop (please complete the following information):

  • OS: [linux ubuntu]
  • Browser [chrome]
  • Version [Version 91.0.4472.114 (Official Build) (64-bit)]

feat: something like Svelte actions

Is your feature request related to a problem? Please describe.
Not applicable.

Describe the solution you'd like
I'd like to use something like Svelte actions in Million, e.g.:

// This is an action
const tooltip = (node, { text }) => {
  // Mount the tooltip (i.e. create)

  return {
    update: ({ text }) => {
      // Update the tooltip with its new text
    },
    destroy: () => {
      // Unmount the tooltip
    },
  }
}

// This is the action being used
html`<button ${tooltip({ text: 'Some explanation' })}>Do something</button>`

Describe alternatives you've considered
Maybe it can be implemented with a driver? Not sure yet, I discovered Million less than 24h ago.

Additional context
https://svelte.dev/docs#template-syntax-element-directives-use-action

`el` is undefined when attempting to overwrite

When trying to add children to something, el seems to be undefined. Unsure whether this is child or rootnode

image

import { patch, _ } from '../src/index';
import helpers from './helpers';
const { div } = helpers;

const children = [];

children.push(div(_, [String(Date.now())]));
patch(div(_, children), document.querySelector<HTMLElement>('#app'));
children.push(div(_, [String(Date.now())]));
patch(div(_, children), document.querySelector<HTMLElement>('#app'));

Question: patch performance.

The patch algorithm with no delta info is quite simple and it's easy to make a worst case.

e.g. If we patch A B C โ†’ X A B C, this algorithm would compare & patch old_C with B, then old_B with A, then old_A with X, then insert C (4 steps). While the best way to do this is one step: insert X before old_A.

Better algorithms can be found in inferno, solid, fre and vue. (The solid.js one should be easiest to read.)

You may argue that using delta is a better choice, it's the same to say we left a blank in dom diffing to let other programmers write. Besides, if we're making some ui framework which can analyse template like vue or svelte, then the delta info can be cached and the performance is the best. -- only if we have template. If we choose to work like react, whose layout is determined at runtime (the render() func), then we get no way to pre calculate the delta, neither caching it.

Would you implement a better patch algorithm, or leave it as is?

feat: take million router implementation to next level

Is your feature request related to a problem? Please describe.
This feature is to take million/router to next level

Describe the solution you'd like
My presented solution is to make url prefetching and diffing faster by doing it ahead of time and parallel in a web worker. Users can present links with a dummy attribute to recognise the priority of resource. All the links in the current page view will be collected and sent off to the web worker where it will request and cache resources based on priority. On caching, high priority resources can be made into a vdom representation in the worker and sent back to the main thread where it will use the watchMedia function to render it in a template tag. When the user clicks that url, simply the template tag can be brought in. This will make mpa traversing faster. Additionally it can even provide some transitions animation technique support like FLIP

Describe alternatives you've considered
turbodrive seems to provide similar functionality but with millionjs perf, ahead of time parallelized caching and concurrent morphing of the template dom based on the availability of main thread we will be better than turbodrive. Also FLIP support and minimal size will be good to have add-ons

Bug: JSX namespace not being recognized in vite TSX context

Describe the bug

`JSX element implicitly has type 'any' because no interface 'JSX.IntrinsicElements' exists.` error with jsx and vite.

To Reproduce

1. Setup a vite project with this config: 

import { defineConfig } from 'vite';

export default defineConfig({
  esbuild: {
    jsxFactory: 'jsx',
    jsxFragment: 'Fragment',
    jsxInject: `import { jsx, jsxs, Fragment } from 'million/jsx-runtime'`,
  },
});

2. Create a `index.tsx` and `index.html`, and import the tsx file.
3. Write some jsx
4. View intellisense errors

Expected behavior

Should not have this error and vite should use the JSX namespace

Screenshots

N/A

Operating System

MacOS Monterey

Browser

Brave 1.29.77 Chromium: 93.0.4577.63 (Official Build) (arm64)

Additional context

N/A

feat(ssr): virtual node to html

Is your feature request related to a problem? Please describe.
SSR needs to get a static html string for prerendering. Currently, Million uses a very naive implementation:

export const fromVNodeToString = (vnode: VNode): string => {

This implementation misses a lot of edge cases (like events, object attribute value, etc)

Describe the solution you'd like
A simple, one function solution that handles all edge cases for SSR.

Bug: Unable to import million/block (Module not found: Error: Can't resolve 'packages/million/m')

Describe the bug

Getting following errors while importing Block from million (packages doesn't exist)

 Module not found: Error: Can't resolve 'packages/million/m'

 resolve as module
      /node_modules/million/dist/node_modules doesn't exist or is not a directory
      /node_modules/million/node_modules doesn't exist or is not a directory
     /node_modules/node_modules doesn't exist or is not a directory

To Reproduce

import { block } from 'million/block';

Expected behavior

Should work as docs

Screenshots

No response

Operating System

Ubuntu

Browser

FireFox

Additional context

It's the simple webpack 5 project, other than Block all methods works from million.js

feat: `diff` function to support exporting the difference between the two vnodes

Describe the solution you'd like
I found that nearly all of the vdom frameworks support effecting the dom directly, but don't support exporting the difference between the two vnodes. I thought maybe it would be useful when we want to get difference between the two vnodes and render them to the website for visualization by the custom styles, just like the git diff styles that help us to diff the two versions of our code.

I thought that we can set the diff of props for all vnodes, and export the two diffed vnodes tree as result. Maybe it would be very interesting and help us know where we have updated.

Feature: Add Matt-Esch/virtual-dom to benchmarks

Is your feature request related to a problem? Please describe.

It would be nice if there were [Matt-Esch/virtual-dom](https://github.com/Matt-Esch/virtual-dom) benchmarks.

Describe the solution you'd like

[Matt-Esch/virtual-dom](https://github.com/Matt-Esch/virtual-dom) benchmarks under the benchmark/ folder

Describe alternatives you've considered

N/A

Additional context

No response

bug: rendered `_` in the dom tree.

Describe the bug
A clear and concise description of what the bug is.
image

maybe caused by here:
image

To Reproduce
Steps to reproduce the behavior:

  1. Go to '...'
  2. Click on '....'
  3. Scroll down to '....'
  4. See error

Expected behavior
A clear and concise description of what you expected to happen.

should render span or other text node in the dom tree?
Screenshots
If applicable, add screenshots to help explain your problem.

Device (please complete the following information):

  • OS: [e.g. iOS]
  • Browser [e.g. chrome, safari]
  • Version [e.g. 22]

Additional context
Add any other context about the problem here.

bug: fix double rendering issue

Describe the bug
A clear and concise description of what the bug is.

To Reproduce
Steps to reproduce the behavior:

  1. Go to '...'
  2. Click on '....'
  3. Scroll down to '....'
  4. See error

Expected behavior
A clear and concise description of what you expected to happen.

Screenshots
If applicable, add screenshots to help explain your problem.

Device (please complete the following information):

  • OS: [e.g. iOS]
  • Browser [e.g. chrome, safari]
  • Version [e.g. 22]

Additional context
Add any other context about the problem here.

Bug: export 'kebab' not found in './million'

Describe the bug

While compiling with webpack/babel I get the following warning:


WARNING in ./node_modules/million/dist/jsx-runtime.esm.js 25:14-19
export 'kebab' (imported as 'kebab') was not found in './million' (possible exports: DELETE, INSERT, OLD_VNODE_FIELD, UPDATE, VFlags, childrenDriver, className, createElement, flush, flushWorkStack, init, m, nextTick, patch, propsDriver, schedule, shouldYield, style, svg)

I think this is pretty straight-forward?



### To Reproduce

```markdown
This should be a basic install of webpack, babel, million, etc. The build does appear to work, in spite of the warning.

Expected behavior

The build should succeed without warnings.

Screenshots

No response

Operating System

MacOS Catalina 10.15.7 (19H1419)

Browser

Terminal build-step

Additional context

Relevant parts of `package.json`:


"devDependencies": {
    "@babel/core": "^7.15.5",
    "@babel/preset-env": "^7.15.6",
    "webpack": "^5.54.0"
  },
  "dependencies": {
    "@babel/plugin-transform-react-jsx": "^7.14.9",
    "babel-preset-million": "^0.0.1",
    "million": "^0.6.1"
  }

Webpack config:

module.exports = {
    entry: {
        'time-sheet': './resources/js/time-sheet.js'
    },
    output: {
        filename: '[name].js',
        path: __dirname + '/public/js'
    },
    resolve: {
        extensions: ['*', '.js', '.jsx']
    },
    module: {
        rules: [
            {
                test: /\.jsx?$/,
                exclude: /node_modules/,
                use: ['babel-loader']
            }
        ]
    }
};

.babelrc

{
    "presets": [
        [
            "@babel/preset-env",
            {
                "modules": "auto",
                "targets": {
                    "browsers": [
                        "> 2% in US",
                        "last 2 iOS major version",
                        "last 2 Safari major version",
                        "last 2 Edge major version",
                        "Firefox ESR",
                        "not dead"
                    ],
                    "uglify": true
                }
            }
        ]
    ],
    "plugins": [
        [
            "@babel/plugin-transform-react-jsx",
            {
                "runtime": "automatic",
                "importSource": "million"
            }
        ]
    ]
}

Bug: the tag of newNode with props/children is not patched

Describe the bug
Review this part of the code.

million/src/patch.ts

Lines 124 to 133 in 9def880

if (
(<VElement>oldVNode)?.tag !== (<VElement>newVNode)?.tag &&
!(<VElement>newVNode).children &&
!(<VElement>newVNode).props
) {
// newVNode has no props/children is replaced because it is generally
// faster to create a empty HTMLElement rather than iteratively/recursively
// remove props/children
return replaceElementWithVNode(el, newVNode);
}

To Reproduce
You can run this code in a plain html page.

import { m, createElement, patch } from "https://cdn.jsdelivr.net/npm/[email protected]"
let app = m('h1', undefined, ['hello'])
let el = createElement(app)
document.body.append(el)
console.log(el.outerHTML) // <h1>hello</h1>
let next = m('div', undefined, ['world'])
patch(el, next)
console.log(el.outerHTML) // <h1>world</h1>

Expected behavior
The final result of el.outerHTML should be <div>world</div>

`browser` field has higher priority than `module`

Describe the bug
The package.json is wrong. browser field is not only can be used to tell cdn to use this file, but also tells bundlers (webpack, rollup, etc.) to use it over the module one.

This prevents bundlers to use the esm file. The browser field is to create dual modules which both supports node.js and browser but need a bit different code.

You can checkout axios for an example, which is not m have to worry about since it can only run on the browser.

The correct package.json should be:

  "main": "dist/million.umd.js",
  "module": "dist/million.esm.js",
  "jsdelivr": "dist/million.min.js",
  "unpkg": "dist/million.min.js",
  "types": "dist/million.d.ts",
  "exports": {
    "require": "./dist/million.umd.js",
    "import": "./dist/million.esm.js"
  },

bug: `Cannot set property form of #<HTMLButtonElement> which has only a getter`

Describe the bug
When trying to create a button element with the form attribute, the following error happens:

Uncaught TypeError: Cannot set property form of #<HTMLButtonElement> which has only a getter

To Reproduce

  1. Open https://stackblitz.com/edit/mega-millions-ryy3r5?file=index.html
  2. See the error in the console

Expected behavior
It should be possible to create a button element with the form attribute just as it's possible to do this:

document.body.innerHTML = '<button form="some-form">OK</button>'

Maybe the form attribute (and possibly others too) can only be set with setAttribute()?

Screenshots
image

Device (please complete the following information):

  • OS: macOS
  • Browser: Chrome
  • Version: 102

Additional context

} else if (el[propName] !== undefined && !(el instanceof SVGElement)) {
el[propName] = propValue;
} else {

feat(react): component level render

Currently, Million renders the whole UI (through the compat function. It would be more efficient to have "component level renders", where the hook function is invoked on each component and setState would rerender only the component and its children. This would also effectively remove the need for the compat function

I'm having trouble implementing this, any help would be greatly appreciated.

Is this me, or is this form of setter the issue?

Discussed in https://github.com/aidenybai/million/discussions/144

Originally posted by rk October 28, 2021
So, I've had this happen several times in various places. And it appears to stem from assuming that the object representing the row is passed by reference, so that changes to that object are kept with it even if sort changes its position. Here's a contrived example:

https://replit.com/@rk57/LoyalSafeBoastmachine#script.js

For simplicity, the "setter" takes in the row object and the field (key on the object) being permuted by the input, and it returns an event handler method. The handler sets the value on the row, sorts in place, triggers the patch/update to the DOM, and then attempts to restore focus to the changed date field.

function setter(row, field) {
  return (e) => {
    // Set value:
    row[field] = e.target.value;
    console.log(`set ${row.key}.${field} to ${e.target.value}`);

    // Sorts in place:
    entries.sort((a,b) => (new Date(a.dt)).getTime() - (new Date(b.dt)).getTime());

    // Trigger an update render:
    update();

    // "restore" focus
    document.getElementById(`${row.key}_dt`)
      .focus();
  };
}

This works predictably enough until you overlap dates. Try focusing the middle input and then increasing or decreasing the value twice, so that the months overlap. Now try increasing/decreasing to add/remove overlap ... and the value in the overlapped input changes too! Even in the data logged to the console!

set 2.dt to 2021-05-28
[ { key: 0, dt: '2021-02-28' },
  { key: 1, dt: '2021-05-28' },
  { key: 2, dt: '2021-05-28' },
  { key: 3, dt: '2021-08-28' },
  { key: 4, dt: '2021-10-28' } ]

I have noticed that sometimes the event fires twice, possibly because the event listener is being added without removing the old? Is this me?

feat: give monorepo a hug

Is your feature request related to a problem? Please describe.
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
just a discuss, is there any plan about monorepo?
Describe the solution you'd like
A clear and concise description of what you want to happen.

Describe alternatives you've considered
A clear and concise description of any alternative solutions or features you've considered.

Additional context
Add any other context or screenshots about the feature request here.

bug(react): nest components in components do not work

Describe the bug
Nested components, only at the root level of the parent component, will not work.

To Reproduce

import { useState, Component } from 'react';
import { createRoot } from 'react-dom/client';

function Btn() {
  const [count, setCount] = useState(0);

  return (
    <button
      onClick={() => {
        console.log('click');
        setCount(count + 1);
      }}
    >
      {count}
    </button>
  );
}

function App() {
  return <Btn count={0} />;
}

createRoot(document.body).render(<App />);

Will not increment when clicked

bug: Wouter compat

Describe the bug
Wouter errors when integrated into a Million/React environment

To Reproduce

import { createRoot, useState } from 'million/react';
import { Link, Route } from 'wouter';

function App() {
  const [count, setCount] = useState(0);
  return (
    <div>
      <button onClick={() => setCount(count + 1)}>{count}</button>

      <Link href="/users/1">
        <a className="link">Profile</a>
      </Link>

      <Route path="/about">About Us</Route>
      <Route path="/users/:name">{(params) => <div>Hello, {params.name}!</div>}</Route>
    </div>
  );
}

createRoot(document.body).render(<App />);

Gives something like this:

wouter.js?v=b667f2ca:1129 Uncaught TypeError: Cannot read properties of null (reading 'i')
    at useRef (wouter.js?v=b667f2ca:1129:14)
    at useNavigate (wouter.js?v=b667f2ca:1636:18)
    at Link (wouter.js?v=b667f2ca:1659:18)
    at compat.ts:15:17
    at hook2 (hooks.ts:37:17)
    at Object.createComponent [as handleFunction] (compat.ts:39:4)
    at Object.h (h.ts:45:14)
    at App (script.tsx:15:19)
    at compat.ts:15:17
    at hook2 (hooks.ts:37:17)

Expected behavior

Should behave as Wouter intends it to

Feature: patch return new dom element

Is your feature request related to a problem? Please describe.

If we patch a vnode to some element with different tag name,
the original element will be removed and be replaced with a new element.
The new element can not be accessed unless we do a `querySelector`.

Describe the solution you'd like

Let `patch()` returns the new element, so that we can keep a reference
to the real dom.

Describe alternatives you've considered

None.

Additional context

No response

Request: support setting data/aria attributes

Is your feature request related to a problem? Please describe.

So, I've been trying to figure out how to associate nodes created by million with data in the rest of the script. Note that for this use-case I'm creating a menu builder using dragula to allow drag and drop ordering/nesting. As such, it's simpler to leverage the DOM to maintain the tree than in-memory within the script. Just query the parent wrapper, then walk down the nodes/children to reassemble the data. Because there are multiple layers and attributes to track, it's simplest to keep this data in a data-attribute.

<div class="menu-node" data-config='{"title":"foo","url":"/bar/","publish":true}'>
  <div class="menu-item">
    <span class="menu-item-title">foo</span>
    <span class="menu-item-url">/bar</span>
    <span class="menu-item-publish icon icon-eye"></span>
  </div>
  <!-- child nodes go here -->
</div>

Technically, setting a property like "data-config" works but I'm unsure how it'll be persisted. But this is a symptom of a another problem:

  • What if I added a link and need data-toggle="modal" data-target="#editModal" to be set on each item for a Bootstrap modal dialog? (I know I can set a .js-edit-modal class and an href of #editModal with a tiny bit of script to alleviate this.) I'm sure there are other 3rd party libraries which need data attributes that we cannot work around.
  • There is no way to assign arbitrary attributes because element.attributes is read-only. This also means that we cannot set aria- attributes for accessibility!

Describe the solution you'd like
I'm pretty sure we need a way to set arbitrary attributes at least, which would also handle data attributes.

Describe alternatives you've considered
For my own use-case I've considered using a WeakMap to maintain the data. But with patch(), but making it work with both an HTMLElement and a VNode is more complex. So I've thought about generating a random key to pass into patch, and using that string to associate it with a Map (it loses the automatic deletion of a WeakMap however).

No matter what alternative I'm considering, it's still more complex than a data attribute.

VFlag Import Undefined

Describe the bug
Importing VFlags leads to import errors with Webpack

Expected behavior
Just being able to import VFlags from Millionjs to use with m function

Screenshots
image

Desktop (please complete the following information):

  • OS: Manjaro
  • Browser: Firefox

bug: `fromStringToVNode` type defination would cause error

Describe the bug
I found that the type defination of new utils function fromStringToVNode is declare const fromStringToVNode: (htmlString: string) => VNode.Then I tested it with fragments, but I found the type of the return value is VNode[].

I think the incorrect type defination would cause TypeScript type error.

To Reproduce
Steps to reproduce the behavior:

  1. try any fragments with fromStringToVNode transformer API

Expected behavior
I think the type defination of fromStringToVNode can be defined as declare const fromStringToVNode: (htmlString: string) => VNode[] instead, or declare const fromStringToVNode: (htmlString: string) => VNode | VNode[](but would write more code then to handle the different types, so I perfer the former)

Bug: elements with a key are not updated

Describe the bug

When the contents of an element which has a key change, the DOM is not updated. The same behaviour is observed either when using ONLY_KEYED_CHILDREN or without it.

To Reproduce

1. In the following example click on the "Update" button.
2. Notice the new node is appended but the old one is not changed.


<!DOCTYPE html>
<body>
<button id="update">Update</button>
<script type="module">
import {m, createElement, patch, VFlags} from 'https://unpkg.com/million?module';
const element = createElement(m('div', undefined, [m('div', {key: 'a'}, ['old'])], VFlags.ONLY_KEYED_CHILDREN));
document.body.appendChild(element);
document.getElementById('update').addEventListener('click', () => {
	const vnode = m('div', undefined, [m('div', {key: 'a'}, ['changed']), m('div', {key: 'b'}, ['new'])], VFlags.ONLY_KEYED_CHILDREN);
	console.log(vnode);
	patch(element, vnode);
});
</script>
</body>

Expected behavior

The DOM should be changed to:

changed
new

Screenshots

No response

Operating System

MacOS

Browser

Google Chrome 96

Additional context

No response

[BUG] Taking syntax error when running the millionjs sample.

My node.js code:

const { m, createElement, patch } = require('million');

const app = createElement(m('div', { id: 'app' }, ['Hello World']));

console.log(app);

My node version is 12.16.1.
npm is 6.13.4.

Error:

...\node_modules\million\dist\million.umd.js:78
      if (props?.key) {
                ^

SyntaxError: Unexpected token '.'
    at wrapSafe (internal/modules/cjs/loader.js:1072:16)
    at Module._compile (internal/modules/cjs/loader.js:1122:27)
    at Object.Module._extensions..js (internal/modules/cjs/loader.js:1178:10)
    at Module.load (internal/modules/cjs/loader.js:1002:32)
    at Function.Module._load (internal/modules/cjs/loader.js:901:14)
    at Module.require (internal/modules/cjs/loader.js:1044:19)
    at require (internal/modules/cjs/helpers.js:77:18)
    at Object.<anonymous> (C:\Users\Furkan\Documents\GitHub\Kapseli-UI-Framework\test.js:1:37)
    at Module._compile (internal/modules/cjs/loader.js:1158:30)
    at Object.Module._extensions..js (internal/modules/cjs/loader.js:1178:10)
```

Bug: inconsistent performance when adding this extra header to page

Describe the bug

In the timesheet entries, I have the system set up to render the rows and it all works great. DOM operations for insert/delete/update are roughly 30ms total. Then in some contexts I need to add a header that summarizes the data for the timesheet. It's really just status and totals, because the totals may update when changes are made and that feedback can be helpful to the user.

Please note that generating the VNode tree takes approximately 1ms both with and without the following header info. When the header is generated it adds 90ms - 400ms to the DOM operations, dependent on how its rendered. Upgrading to v0.6.4 has dropped the times from about 300ms - 550ms originally, so it is better than before.

Also note that all interpolated areas/values in the following JSX code are already converted to strings. No additional VNodes are created, just these text nodes.

Original form (180ms average increase in time):

<div className="row mb-4">
    <div className="col-auto">
        <strong>User</strong>
        <br />
        {sheet.user.name}
    </div>
    <div className="col-auto">
        <strong>Time Sheet Period</strong>
        <br />
        {sheet.week_start.format()} - {sheet.week_end.format()}
    </div>
    <div className="col-auto">
        <strong>Total Points</strong>
        <br />
        {total_points} Points
    </div>
    <div className="col-auto">
        <strong>Total Hours</strong>
        <br />
        {total_hours} Hours
    </div>
    <div className="col-auto">
        <strong>Status</strong>
        <br />
        <span className={statusClasses}>{sheet.status}</span>
    </div>
</div>

"Optimized" form (90ms average increase in time):

<div className="row mb-4">
    <div className="col-auto">
        <div className="text-bold">User</div>
        <div>{sheet.user.name}</div>
    </div>
    <div className="col-auto">
        <div className="text-bold">Time Sheet Period</div>
        <div>{sheet.week_start.format()} - {sheet.week_end.format()}</div>
    </div>
    <div className="col-auto">
        <div className="text-bold">Total Points</div>
        <div>{total_points} Points</div>
    </div>
    <div className="col-auto">
        <div className="text-bold">Total Hours</div>
        <div>{total_hours} Hours</div>
    </div>
    <div className="col-auto">
        <div className="text-bold">Status</div>
        <div><span className={statusClasses}>{sheet.status}</span></div>
    </div>
</div>

I think the issue relates to checking text nodes, as well as empty nodes like <br/> tags.

I have also noticed a performance hit in a different area, related to updating select and option tags. (Inserting/updating a select with 1000 items can take 1 second.) But I can do a little checking and file a separate issue for that.

To Reproduce

The VNode tree looks a bit like this:

  • Wrapper element (never changes)
    • "Sheet Header" (see above)
    • "Card" element
      • Header (never changes)
      • [ multiple rows here ]
      • Footer (never changes)
        • Add Type 1 (button)
        • Add Type 2 (button)

As you'd guess, the bulk of the changes are in the "multiple rows" area. When the "Sheet Header" is included in the context, performance immediately degrades from a stable average of 30ms.

Expected behavior

I expected only a minor performance hit, if at all.

Screenshots

Not really visually related.

Operating System

MacOS Catalina 10.15.7 (19H1419)

Browser

Chrome 94; FireFox 93

Additional context

If it would help, I could try recording flame graphs.

Performance hits between contexts that I understand and expect for the moment:

  • Select and Option tags have varied values and counts for each; I think it can account for up-to 10ms loss right now. (Now that I have accounted for the 1k element select per row issue.)

bug: error when using `xmlns:xlink`

Describe the bug
When trying to create an <svg> element with the xmlns:xlink attribute, the following error happens:

Uncaught DOMException: Failed to execute 'setAttributeNS' on 'Element': 'http://www.w3.org/1999/xlink' is an invalid namespace for attributes.

To Reproduce

  1. Open https://stackblitz.com/edit/mega-millions-iqrsnb?file=index.html
  2. See the error in the console

Expected behavior
It should be possible to create an <svg> element with the xmlns:xlink attribute just as it's possible to do this:

document.body.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"></svg>'

Screenshots
image

Device (please complete the following information):

  • OS: macOS
  • Browser: Chrome
  • Version: 102

Additional context

} else if (propName.charCodeAt(5) === COLON_CHAR) {
el.setAttributeNS(XLINK_NS, propName, String(propValue));
}

Bug: why does using keys produce seemingly random duplicates?

Describe the bug

The deeper you go, the more likely you are to hit this issue. For instance, a top-level wrapper with ONLY_KEYED_CHILDREN and unique keys appears to work fine. But nest it within another element as a wrapper, and duplicates start appearing after adding maybe 3 items.

Reproducing this in a basic page like this was a nightmare. ๐Ÿฅต I still haven't actually reproduced the issue I was originally troubleshooting, which was adding 6-8 duplicate items when inserting one (deleting it from the context/state deleted all 6-8 dupes of the row).

Here's an example:

<!DOCTYPE html>
<body>
<div id="app"></div>

<script type="module">
  import {
    m,
    patch,
    VFlags
    /* or any other exports you want to access */
  } from 'https://unpkg.com/million?module';

  const format = new Intl.DateTimeFormat('en-us', { dateStyle: 'short', timeStyle: 'medium' });
  let dates = [];

  function render() {
    dates.push(new Date());

    let vdom = m(
      'div',
      { id: "app" },
      [
        m('div', undefined, dates.map(dt => m('div', { key: dt.getTime().toString() }, [format.format(dt)])), VFlags.ONLY_KEYED_CHILDREN)
      ]
    );

    console.log('render', vdom);

    patch(document.getElementById('app'), vdom);
  }

  setInterval(render, 1000);
  render();
</script>
</body>

To "fix" this behavior, just remove the VFlags.ONLY_KEYED_CHILDREN and the default diffing works fine. Even more strange: remove the .wrapper element and make the date entries direct children of the #app element and it works again.

To Reproduce

This behavior appears to be part of the diffing when using keys with ONLY_KEYED_CHILDREN. What confuses me is why it only happens after it's nested below the top-level. I've spent time trying to review the children driver for diffing, and haven't noticed anything obvious.

To reproduce this:

  • Nest the children one level deeper than the top, or more.
  • Add keys on each element.
  • Add VFlags.ONLY_KEYED_CHILDREN on the parent/wrapper node.

Expected behavior

Yeah, only 1 item should be added per keyed item.

Screenshots

Screen Shot 2021-10-22 at 12 10 11 PM

Operating System

MacOS Catalina 10.15.7 (19H1419)

Browser

Chrome 94; FireFox 93

Additional context

No response

'm' cannot be used as a value because it was exported using 'export type'.

Describe the bug
As the title states

To Reproduce

import { m, createElement } from "million";

const el = createElement(m("div", {}, ["Hello world"]));

Expected behavior
Everything compiles as it should.

Screenshots
image

Additional Context
It looks like in the million.d.ts file, m, createElement, and patch are being exported as types and not as functions

feat: Router to highjack forms as well as anchors

Is your feature request related to a problem? Please describe.
would be super helpful if router handled form submissions as well. I think this would make it feel much more "SPA like" than just doing anchor links. Many times this is what folks resort to a SPA for dealing with form validation ect...

Describe the solution you'd like
Similar to how it handles clicks on the window, if it handled submit.

window.addEventListener("submit", async (event) => {
  event.stopPropagation()
  ev.preventDefault()

  const body = Array.from(new FormData($form)).reduce((acc, [key, val]) => {
    acc[key] = val
    return acc
  }, {})

  const opts = {
    method: $form.method,
    headers,
    redirect: 'follow'
  }

  let url = $form.action

  if ($form.method.toLowerCase() === 'get') url += '?' + new URLSearchParams(body)
  else opts.body = JSON.stringify(body)

  const request = new Request(url, opts)
  const res = await fetch(request)
  // parse the response and patch the DOM....
})

Bug: SVG not created with correct namespaceURI

Describe the bug

We use Icomoon (icomoon.io) for the icons in the app I'm working on, specifically with the SVG spritesheet option. Icons use markup like:

<svg class="icon icon-bell">
  <use xlink:href="/fonts/icomoon/symbol-defs.svg#icon-bell" />
</svg>

From what I'm reading of the documentation, the million version should look like:

function icon(iconName) {
    return svg(m('svg', {
        className: `icon icon-${iconName}`
    }, [
        // SVG 2 support means that HREF should be enough now;
        // otherwise we may need an xlinkHref setter for setAttributeNS
        m('use', { href: `/fonts/icomoon/symbol-defs.svg#icon-${iconName}` })
    ]));
}

From my research into JSX that should look like:

function icon(iconName) {
    return (
        <svg className={`icon icon-${iconName}`}
             xmlns="http://www.w3.org/2000/svg"
             xmlnsXlink="http://www.w3.org/1999/xlink">
            <use xlinkHref={`/fonts/icomoon/symbol-defs.svg#icon-${iconName}`} />
        </svg>
    );
}

However, the icon does not display. The created element's $0.namespaceURI is incorrect.

To Reproduce

Use one of the functions above and inspect the resulting node. I noticed that $0.namespaceURI in the console returns http://www.w3.org/1999/xhtml.

Expected behavior

The created nodes should have the correct SVG namespace. Functionally, the output should be roughly equivalent to this plain JS function:

function icon(iconName) {
    let svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
    let use = document.createElementNS('http://www.w3.org/2000/svg', 'use');

    svg.setAttribute('xmlns:xlink','http://www.w3.org/1999/xlink');
    svg.setAttribute('class', ['icon', 'icon-' + iconName].join(' '));
    use.setAttributeNS('http://www.w3.org/1999/xlink', 'href', '/fonts/icomoon/symbol-defs.svg#icon-' + iconName);
    svg.appendChild(use);

    return svg;
}

I'm not super worried about setAttributeNS support for the JSX style xlinkHref attribute, because SVG2 support is pretty broad. Browsers should support <use href="" /> across the board now.

Screenshots

I'll attach a screen shot in a comment, since drag/drop doesn't work on this field.

Screen Shot 2021-09-29 at 8 39 13 AM

Operating System

MacOS Catalina 10.15.7 (19H1419)

Browser

Chrome 94; FireFox 92

Additional context

I'm actually working on adding a new feature with heavy front-end reactivity using million, so I have a lot of things to test in a real world scenario. I'll probably find a variety of things as I implement the feature. It was Vue3 or Million, and I decided to give million a try. ๐Ÿ˜„

Feature: test

Is your feature request related to a problem? Please describe.

test

Describe the solution you'd like

test

Describe alternatives you've considered

test

Additional context

test

Add Million.js to js-framework-benchmark

Is your feature request related to a problem? Please describe.
Local benchmark on my machine is 3 times slower relatively to others, million results similar to virtual-dom.

Describe the solution you'd like
Add cpu/memory/os and browser version to benchmark page

Describe alternatives you've considered
Use external service for benchmarking or allow user to post here results and then collect them to benchmark page, for example:

MacBook Air M1, 8 GB, Safari v14.1.1:

million x 26,400 ops/sec ยฑ0.69% (24 runs sampled)
virtual-dom x 21,543 ops/sec ยฑ0.54% (65 runs sampled)
vanilla x 3,865 ops/sec ยฑ0.94% (24 runs sampled)
baseline x 81,198 ops/sec ยฑ2.52% (16 runs sampled)
Fastest is baseline

bug: million.js can not handle `children` props with falsy values

Describe the bug

RT.

To Reproduce

<div>{true && undefined}</div>

Expected behavior

render an empty div.

Screenshots

Cannot set property 'children' of #<Element> which has only a getter 

Device (please complete the following information):

Doesn't matters.

Additional context

I am using automatic transform. I have taken a look at the code and found out why:

https://github.com/aidenybai/million/blob/main/packages/jsx-runtime/jsx.ts#L9-L12

Here you only delete props.children when it is a truthy value.

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.