GithubHelp home page GithubHelp logo

justin-schroeder / arrow-js Goto Github PK

View Code? Open in Web Editor NEW
2.3K 29.0 50.0 955 KB

Reactivity without the framework

Home Page: https://arrow-js.com

License: MIT License

JavaScript 2.47% TypeScript 97.53%
declarative reactive rendering ui ux web webcomponents

arrow-js's People

Contributors

andrew-boyd avatar bellisario avatar corecreation avatar eniolajayi avatar fasiha avatar gigabyte5671 avatar haitrungle avatar hugodf avatar jukart avatar justin-schroeder avatar luan-nk-nguyen avatar picklenik avatar rafalkornel avatar thomasperi avatar tlylt avatar vascanera avatar wd2010 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

arrow-js's Issues

How to unwatch

Hey!

Really loving ArrowJS - super easy to work with :)

One issue I have found is no way to "un watch". Take for example:

  • You have some component with an onMount. When mounted you setup a watch on some reactive variable.
  • When the component get's disposed of, that watch keeps running.

It would be really helpful if the watch returned some sort of reference which we could then remove on unmount.

A common time this happens is with multi-page apps. When spanning between pages, elements are constantly getting added and removed from the dom. After a bit you just have a bunch of old watch's sitting out there.

Any suggestions are appreciated!
Thanks!

XSS issue for user generated content

First off, thanks for making this toolkit, it is a breath of fresh air. The description of project scope immediately appealed to me. I wish the project well.

Related to #8 Any type of user generated data can contain XSS vulnerabilities. Using a sample from https://cheatsheetseries.owasp.org/cheatsheets/XSS_Filter_Evasion_Cheat_Sheet.html <IMG SRC=/ onerror="alert(String.fromCharCode(88,83,83))"></img>

Which gives you:

I think you should put a warning at minimum on the docs/readme until all known XSS issues are resolved. Thanks again for the project and its inspiring scope.

w: not printing all outputs?

Hey, awesome work! Excuse my ignorance, I am a newbie with frameworks, proxies etc..! I've got a couple of questions..

  1. Why doesn't the watcher log all the values in example below and just logs the first and the last one? I thought it would do something like $on/$off but with just a one-liner..
  2. Since the watcher watches, why log immediately the function (for 25*10) since the data hasn't changed?
  3. From the docs, I couldn't understand the use of $off in the example! Thanks!! Great work!
const data = r({
  price: 25,
  quantity: 10,
  logTotal: true
})

function total () {
  if (data.logTotal) {
    console.log(`Total: ${data.price * data.quantity}`);
  }
}

w(total)

data.price = 35
data.price = 66
data.price = 150

Typescript support for callable ReactiveProxy<T>?

I'm curious to know if ES6 class methods and other callback functions are officially supported by the r() function, or if they only work by coincidence.

I'm asking because the ReactiveProxy<T> type isn't callable, which means you get warnings/errors when using TS, but not when using JS. It also appears to work fine, so I'm not sure if it is intended behavior or not.

Here's an example of what I mean with an ES6 class:

class Foo {
  public mutateState(): void {
    // mutates internal class state here
  }
}

// ...

const data = r(new Foo())
data.mutateState(); // <-- This will get a Typescript warning/error

It yields the following warning/error:

  Type 'ReactiveProxy<() => void>' has no call signatures.

The same thing happens if I pass a function definition into the r() function instead of a new class (i.e. r({greet: () => alert('hi')}))

Like I said, though, having it mutate its own data works as expected (and even causes re-renders when placed in a template) if I tell Typescript to ignore the issue.

If it's intended to work, then maybe the ReactiveProxy<T> type needs a way to be callable?

If passing mutators into the r() method isn't intended, it would be useful to have it added to the docs.

Array of text boxes without a good key?

First off, thank you SO MUCH for your work on this project! I've gone back and forth on every reactivity framework I could find over the past few weeks, and all of them were either too limited or (WAY) too complex to set up. ArrowJS is a huge breath of fresh air! (Not to mention, this is a work project and I was really stressing that I was going to miss my deadline, either because I couldn't find an appropriate framework or because doing it all by hand was taking a very long time.)

So I'm trying to do this:

<fieldset>
    ${() => addressData.streets.map(
            (streetData, index) => html`
            <div style="margin-bottom: 5px;">
                <input type="text" required value="${streetData}" @input="${e => addressData.street[index] = e.target.value}"/>
                <button type="button" @click="${() => removeStreet(index)}">Remove Street</button>
            </div>
        `.key(index)
    )}
    <button type="button" @click="${() => addStreet()}">Add Street</button>
</fieldset>

The streets array is a plain array of strings. I can't easily change the data, so I'm trying to find a way to work with it as-is. All my other arrays are arrays of objects that have database IDs that I can use as keys, but streets is stored in a weird way in the DB.

If I don't do key(), I have to re-focus the input box after typing each character. I assume this is because all children of <fieldset> are being removed and recreated when the data changes. If I do key(index), the behaviour while typing is jank (first input needs to be refocused, others don't) and I assume removing streets will be broken (the remaining inputs won't get updated properly).

What's the workaround for this situation? Is there something I'm missing or do I just need to get creative?

Add documentation for keys (list rendering)

If you have a list of data elements and then change the order of them, the DOM gets out of sync and can render more elements than actually exist in the data.

I wrote a simple todo app with the ability to mark a todo as done. Done todos are sorted to the bottom.

I only had four total todos and marked them done in an odd order. Once I marked them all as done, I got duplicate DOM elements leading to 6 list items, even though the data list still only had 4 items.
This doesn't alway happen at the same time, but it does happen consistently.

image

I think a "key" is needed to track identical DOM elements across renders, like React.

import { r, t, w } from "@arrow-js/core";

type Todo = {
  label: string;
  isDone: boolean;
};

const App = () => {
  const data = r({
    todos: [] as Todo[],
  });

  w(() => console.log("todos changes", JSON.stringify(data.todos)));

  const addTodo = (label: string) => {
    console.log("add todo", label);
    const newTodo: Todo = { label, isDone: false };
    data.todos = [...data.todos, newTodo] as any;
  };

  const markDone = (todo: Todo) => () => {
    console.log("mark done", todo);
    data.todos = data.todos.map((t) =>
      t === todo ? { ...t, isDone: true } : t
    ) as any;
  };

  const sortedTodos = () => {
    return [...data.todos].sort((a, b) => {
      if (a.isDone === b.isDone) {
        return 0;
      }
      return a.isDone ? 1 : -1;
    });
  };

  return t`
        <h1>Todo List</h1>
        <input type="text" @keyup="${(e: KeyboardEvent) => {
          if (e.code === "Enter") {
            const target = e.target as HTMLInputElement;
            addTodo(target.value);
            target.value = "";
          }
        }}">
        <ul>
        ${() =>
          sortedTodos().map((todo) =>
            TodoItem({ todo, markDone: markDone(todo) })
          )}
        </ul>
        `;
};

const TodoItem = ({ todo, markDone }: { todo: Todo; markDone: () => void }) => {
  const style = () => {
    if (todo.isDone) {
      return `text-decoration: line-through`;
    }
    return "";
  };

  return t`
        <li style=${style}><button @click=${markDone}>Done</button>${todo.label}</li>
    `;
};

const appElement = document.getElementById("app");
App()(appElement);

[Question] Are there ways to integrate with hyperscript interfaces?

Love the ideas and approach of arrow-js. I believe this tiny thing is going to be the next big thing for many developers who are interested in committing to JS and away from JS frameworks.

I have a question if arrow-js or html templates have possible integration points with hyperscript's h interface. Since h is the underlying interface for many other JS frameworks e.g. React.createElement, Vue.h, Mithril.m, and is also agnostic, being able to integrate/map with html in some ways would open up adoption paths to arrow-js from other frameworks.

If this should live outside of arrow-js, is there a general direction how arrow-js.html can be mapped to h?

type H = <P extends object>(
  tag: string,
  props: P,
  children: H | H[],
);

// how to map between "arrow.html <-> h"
// example html's `@click` and `@input` mappable to `props[click|onClick]` and `props[change|onChange]` based on some mapping object?

Thank you for your time.

Binding reactively to array length

Say I have this:

const selectedProductIds = reactive([]);
return html`
    <div>
        <span>Selected products: ${() => selectedProductIds.length}/${productCount}</span>
    </div>

    ...

    <input type="checkbox" ${selectedProductIds.includes(productData.internalId) ? 'checked' : ''} @input="${e => {
        if (e.target.checked) {
            if (!selectedProductIds.includes(productData.internalId)) {
                selectedProductIds.push(productData.internalId);
            }
        } else {
            if (selectedProductIds.includes(productData.internalId)) {
                selectedProductIds.splice(selectedProductIds.indexOf(productData.internalId), 1);
            }
        }
        selectedProductIds.length = selectedProductIds.length + 1; // <----- how can I not have to do this?
        selectedProductIds.length = selectedProductIds.length - 1;
    }}"/>

How can I avoid having to do the jankiness of those last two lines? If I only push to the array or splice from it, the reactive length binding doesn't update.

Keyed lists causing re-render

I have just tried the library and it's quite impressive for a such small code but I'm having an issue with list and map. We can use .key() to avoid creating new item but the problem happens when we use

  return t`
      <ul>
        ${() => state.todos.map((todo) => TodoItem(todo).key(todo.id) )}
      </ul>
  `;

The problem with this loop, it will rerender every list item even if we only edit one element todos[1].title for instance.

Expectation

The system could proxy the map méthod to "memo" the value and only rerun the method when the item in the array changed.

Static string cleared from attribute

Example:

const data = reactive({
  class:'css'
})
<li class="prefix-${() => data.class}-postfix" >Title</li>

renders to:

<li class="css" >Title</li>

instead of:

<li class="prefix-css-postfix" >Title</li>

notice "prefix/postfix" removed from the class attribute.

I understand that this can be refactored (to include all inside the function) but I'd expect static string before/after observable to be left unchanged.

This example is oversimplified but the idea is that template holds static values (to render even without dynamic part being set).

Cross-Site Scripting Vectors (XSS)

Hi there! From one lightweight reactive library creator to another, nice work!

Just wanted to flag a few XSS vectors not currently blocked by ArrowJS. The first two alerts show up. The third alert (in the svg) and fourth do not. The janky table format is rendered, as is the link which, if opened, runs an attack.

I'm not sure what your rendering and diffing engine looks like under-the-hood, or I'd share a fix, but if you want, feel free to peruse my source code if anything in there is useful to you.

let xss = [
	`<p><img src="x" onerror="alert(1)"></p>`,
	`<p>abc<iframe//src=jAva&Tab;script:alert(2)>def</p>`,
	`<svg><g/onload=alert(3)//<p>`,
	`<math><mi//xlink:href="data:x,<script>alert(4)</script>">`,
	`<TABLE><tr><td>HELLO</tr></TABL>`,
	`<a href="data:text/html;base64,PHNjcmlwdD5hbGVydCgiSGVsbG8iKTs8L3NjcmlwdD4=">test</a>`
];

let template = html`${xss.map(function (elem) {
	return html`${elem}`;
})}`;

template(document.querySelector('#app'));

fails to render with multiple expressions in an attribute

Here is a test which shows the problem:

import { html } from '..'

describe('extressions in attribute', () => {
  it('should render correctly with a single expression in an attribute', () => {
    const parent = document.createElement('div')
    html`<div class="${() => 'class'}" @click="${() => 'click'}">
      ${() => 'body'}
    </div>`(parent)
    expect(parent.innerHTML).toStrictEqual(`<div class="class">
      body
    </div>`)
  })

  it('should render correctly with multiple expressions in an attribute', () => {
    const parent = document.createElement('div')
    html`<div class="${() => 'class1'} ${() => 'class2'}">
      ${() => 'body'}
    </div>`(parent)
    expect(parent.innerHTML).toStrictEqual(
      `<div class="<!--➳❍--> <!--➳❍-->">
      class1
    </div>`
    )
  })
})

Empty tagged template strings seem to break reactivity

Repro: https://codesandbox.io/s/blissful-napier-k5gvjy?file=/src/index.js

I often use ternary operators for conditional rendering, something like

condition ? html`option 1` : html`option 2`

I noticed that when I don't have an "option 2", I can't simply write

condition ? html`option 1` : html``

because that will throw an error when the condition changes. If the condition doesn't change, it renders fine, no matter if the condition is true or false.

My initial workaround was simply filling the second option with a <br>; it fixed the issue and usually didn't cause any problems.
However, recently I also discovered that I can use null as well, and the parent template string will render it without a hiccup, so that's probably the preferred way. You might wanna add that to the docs somewhere?

Anyway, hope the repro helps and you can fix the bug, not that the empty template string solution should be encouraged or anything, but it certainly shouldn't break the whole application either (which it currently does).

I absolutely love arrow, it's been a great experience so far and I've even built and launched a full appliation, fully powered by arrow!
Keep up the good work, and let me know if you would like to hear more about my experience with it and my suggestions for useful additions :)

Poor performance?

I'm very excited about ArrowJS, but I've just came across benchmark, where ArrowJS is one of the slowest.

On the other hand, your performance demo clearly shows ArrowJS is much faster than Vue.

I'm kind of confused...

Incorrect rendering for Reactive on array item

To Reproduce:

<div id=holder></div>

<script type="module">
import { reactive, html } from 'https://cdn.skypack.dev/@arrow-js/core';
let data = reactive({order: [1, 2, 3]});
const temp = html`${() => data.order.map(item => html`${item} `)}`;
temp(document.getElementById('holder'));
data.order[1] = data.order[1] + 100;
</script>

Expected Output
1 102 3

Actual Output
1 102 2 3

Dark mode flashbang 🫣

there's a split-second light mode flash before the dark mode is applied when visiting the https://www.arrow-js.com/ and also when navigating to Docs

one possible solution could be to set html's background-color property based on @media(prefers-color-scheme: dark) in CSS by default and then let JS take over the theming

SSR

Render the component on the server

Include HTMLElement in conditionally rendered template.

I am using a library that produces an HTMLElement that is typically attached to the DOM with appendChild, or similar. How can I include this element in a conditionally rendered template, similar to the tabs example?

I tried the following code, but the element appears to be missing when the watcher is called:

const element = thirdPartyLibrary.getElement();


function injectElement() {
  if (state.selectedPage === 2) {
    document.getElementById("my-element-div")!.appendChild(element as Node);
  }
}

watch(injectElement);

When I change to tab 2, I get:
Uncaught TypeError: Cannot read properties of null (reading 'appendChild')

Allow arbitrary access to HTML element IDL attributes

If I try to make a reactive style value, then the value of the CSS property in the style attribute is overwritten.

1

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>arrow-js</title>
</head>
<body>
  <div id="app"></div>
  <script type='module'>
    import { reactive, html } from 'https://unpkg.com/@arrow-js/[email protected]/dist/index.min.mjs';

    const state = reactive({
      color: 'red'
    });

    html`
      <button style="background-color: ${() => state.color}">click me</button>
    `(document.getElementById('app'))
  </script>
</body>
</html>

FYI: textarea behavior is very funky if you try to put HTML inside it rather than using the 'value' attribute

I tried doing this:

  import { reactive, html } from 'https://esm.sh/@arrow-js/core';
  const data = reactive({value: "123"})
  html`
  <textarea>${() => data.value}</textarea>
  `(document.body);

As you may already know, it doesn't work:

Screen Shot 2023-04-22 at 8 26 09 PM

However, the workaround (use the value attribute) isn't documented in a way that would have pointed me at the correct behavior. I pieced it together from the changelog and an unrelated GitHub issue.

Distinguish text content from HTML content

In templates, is there any way to distinguish plain text content from HTML content? Or does the caller need to always escape arbitrary text values?

As a contrived example, in the docs under the "Event" header, if I type <b>bold! into the text box, bold text appears, so it seems I can insert arbitrary HTML. That's fine for hard-coded values, but seems like a security issue for anything derived from user input.

For what it's worth, I like the minimal, modern JS-based approach of ArrowJS, but this seems like a potential footgun :)

TS error when using r with Uint8Array

Perhaps there is an incomplete definition on ReactiveProxy... ?

import { r } from "@arrow-js/core";

type Data = {
	payload: Uint8Array;
};

const data = r<Data>({
	payload: new Uint8Array(),
});

data.payload = new Uint8Array();
// ^^^^^^^^^ Type 'Uint8Array' is not assignable to type 'ReactiveProxy<Uint8Array>'.

TypeScript type for nested, optional reactive objects is broken

Let's say I make this reactive object:

interface State {
  x?: { y: number };
}

const state = reactive<State>({x: { y: 0 }});

And then I want to observe the value of x.y:

state.x?.$on('y', (val) => console.log(val));

I get a TypeScript error:

Property '$on' does not exist on type '{ y: number; }'

I recognize this example code won't work if the initial value of x is undefined, but if it's present, .$on works fine, so it would be nice if the type system reflected this.

Rewrite a reactive property which has $on event throw error

import { reactive } from "@arrow-js/core";

const data1 = reactive({
  a: 123
});

const data2 = reactive({
  b: 123
});

data2.$on('b', (value) => {
  console.log(value);
})

data1.b = data2;
data1.b = 123;

Above code throw TypeError: Reflect.get called on non-object error

🐛 Bug with two-way databinding

I was trying to use data binding in arrow-js and ran into a state and render out of sync issue. If you enter a value in the first input, and then enter a value in the second, then the value in the first input does not change. The results are in the screenshot below.

2

Syntax Highlighting for HTML template literals

Is it possible to enable syntax highlighting for html inside template literals in any popular editor? (VS Code, Webstorm, etc) Are there instructions on how to do so anywhere? Apologies if this is documented somewhere, I couldn't find it

Element property syntax not working

It appears that IDL attribute handling in Arrow templates is broken in v1.0.0-alpha.9. I expected the following code to call the setter for complexProp on Test and that logging the value in connectedCallback would provide the correct value:

class Test extends HTMLElement {
    #prop;

    set complexProp(newValue) {
        this.#prop = newValue;
    }

    connectedCallback() {
        console.log(this.#prop.bar); // does not log
    }
}

customElements.define("app-test", Test);

let template = html`
    <app-test .complexProp="${{ foo: "something", bar: "something else" }}"></app-test>
`;

template(document.getElementById("app"));

Instead, the value is still undefined, and in the markup, it appears Arrow is treating .complexProp as an attribute and attempting to serialize it as [Object object] instead:

Screenshot 2023-08-28 at 2 36 52 PM

The same issue occurs if you use a class instance variable instead of a setter and if you use an arrow expression in the template instead of a static expression.

Feature request: event helpers (like: @click.outside)

Please provide support for event helpers (à la alpine) in the template function.
Example: <div @click.outside="someHandler" @some_event.some_helper="some_handler"></div>

Thanks and keep up the good work!
Arrow will be (already is) a big deal! 🥇

Incorrect representation of a table

Hello, I'm trying to use arrow-js (1.0.0 alpha 1) to display a table with the tagged templates, and I'm having some trouble on getting it to work, not sure if it's a bug or it's a wrong interpretation of the docs from my side. I have the following code:

import { t } from "https://cdn.jsdelivr.net/npm/@arrow-js/core";
import table from "./js/components/table.js";

let table_header = ["Name", "State"];
let table_data = [
    ["John", "Subscribed"],
    ["Ashley", "Unsubscribed"],
];

console.log(table(table_header, table_data)());

And then, in table.js:

import { t } from "https://cdn.jsdelivr.net/npm/@arrow-js/core";

export default function table(header, rows) {
	return t`
    <table class="table table-hover">
        <thead><tr>${header.map(column => t`<th>${column}</th>`)}</tr></thead>
        <tbody>
            ${rows.map(columns => t`<tr>${columns.map(column => t`<td>${column}</td>`)}</tr>`)}
        </tbody>
    </table>`;
}

I would expect this to generate the following HTML:

<table class="table table-hover">
  <thead>
    <tr>
      <th>Name</th>
      <th>State</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>John</td>
      <td>Subscribed</td>
    </tr>
    <tr>
      <td>Ashley</td>
      <td>Unsubscribed</td>
    </tr>
  </tbody>
</table>

Instead, I'm getting the following, which is not correct. As you can see, the <th> and <td> elements show outside, and before the <table> element:

<th>Name</th>
<th>State</th>
<td>John</td>
<td>Subscribed</td>
<td>Ashley</td>
<td>Unsubscribed</td>
<table class="table table-hover">
  <thead>
    <tr></tr>
  </thead>
  <tbody></tbody>
</table>

Is this a known issue? Am I doing something wrong?

Script throwing "Illegal Invocation" when adding an object to a reactive array

Hi there!

I'm migrating a personal project from Vue to arrow-js, as I love the idea behind this project. I'm using webmidi and when trying to set an array of MIDIInputs to a reactive property, it throws an "Illegal Invocation" right away. The cause of that is that at some point, the reactivity makes the function "onmidimessage" of a MIDIInput to be called without the right context:

import {Message, WebMidi} from 'webmidi';
import {reactive, watch} from "@arrow-js/core";

const midi = reactive({ inputs: [] })

WebMidi.enable()
  .then(() => {
    midi.inputs = WebMidi.inputs
  })

This doesn't happen when using Vue and its ref() function. I think the problem here is that I don't want to make a deep reactive array, but a shallow reactive array, and only when the array is modified, it should call reactive dependencies and update the template.

This happens with native browser objects, like MIDIInput or any DOM object, because there are some functions that should not be modified or called, or because the context is lost.

A reproducible script using DOM, to make the life easy to anyone wanting to reproduce this would be:

<body>
  <button>Hello</button>
  <script>
    const data = reactive({
      buttons: []
    })
    data.buttons = document.querySelectorAll("button")
  </script>
</body>

You can find here a reproducible stackblitz. I'd be happy to collaborate with the project in any way!

w does not support null values

Consider the following modified example from arrow-js.com:

import { r, w } from "@arrow-js/core";

type Data = {
	price: number | null;
	quantity: number;
	logTotal: boolean;
};

const data = r<Data>({
	price: null,
	quantity: 10,
	logTotal: true,
});

function total() {
	if (data.logTotal && data.price !== null) {
		console.log(`Total: ${data.price * data.quantity}`);
	}
}

w(total);

data.price = 35;

Executing the above yields:

TypeError: null is not an object (evaluating 'e.$on')

How does watch function work?

From the docs:

However, the watcher also detects when a dependency is no longer being used by a function and turns $off tracking for those properties.

In our example, this means that when logTotal is false it will not call our total() function again until data.logTotal is set back to true.

The example function in the doc was:

function total () {
  if (data.logTotal) {
    console.log(`Total: ${data.price * data.quantity}`);
  }
}

But what if the function was:

function total () {
  if (data.logTotal == ifMathTheoremIsTrue()) {
    console.log(`Total: ${data.price * data.quantity}`);
  }
}

Isn't knowing when a dependency is used by an arbitrary function reducible to the Halting Problem?

So when can the watcher detect changes, and when can it not?

HTML boolean attributes break dynamic expressions

Boolean attributes such as the checked attribute on a checkbox seem unable to be set by dynamic expressions.

Example:

This renders and behaves as expected when the checked attribute is static:

html`<input type="checkbox"
  ${data.foo ? 'checked' : ''}
  @input="${e => data.foo = e.target.checked}"
/>`

However, if we make the checked attribute dynamic, two things go wrong:

html`<input type="checkbox"
  ${() => data.foo ? 'checked' : ''}
  @input="${e => data.foo = e.target.checked}"
/>`

(1) It renders like this in the browser, with an unchecked checkbox followed by some mangled code as plain text. (The name of the attribute, checked, seems to replace the value of the next attribute, @input.)

☐ @input="checked" />

(2) The @input event doesn't fire when the checkbox is clicked.

Workaround:

Checkboxes can be made to work by making the checked attribute static and then manipulating the checkbox DOM element directly when the value changes, with $on.

Here's a generalized implementation of the workaround that I've only tested with checkboxes:

function booleanHack(data, prop, attr) {
  let hackAttr = `boolean-hack="${prop}"`;
  data.$on(prop, () => {
    document.querySelectorAll(`[${hackAttr}]`).forEach(el => {
      el[attr] = data[prop];
    });
  });
  return hackAttr;
}

html`<input type="checkbox"
  ${booleanHack(data, 'foo', 'checked')}
  ${data.foo ? 'checked' : ''}
  @input="${e => data.foo = e.target.checked}"
/>`

Click handlers not working anymore

I think 1.0.0-alpha.2 broke the click handlers. This code worked with 1.0.0-alpha.1

Now

import { t } from "@arrow-js/core";

t`
    <button @click=${() => console.log('click')}>Click</button>
`(document.getElementById('app'));

renders as

<button @click="<!--➳❍--">&gt;Click</button>

image

And clicking it does nothing

Map, Set, WeakMap, WeakSet can't be wrapped successfully

If this PR gets merged -> #87, the reactivity will not wrap Map, Set, WeakMap, WeakSet (which means they'll be returned without any interception)

Without it being merged I don't think they work properly inside of reactive(), the following throws:

const data = reactive({
  map: new Map()
})
data.map.size

Error in the browser (Firefox): Uncaught TypeError: get size method called on incompatible Proxy

Output in a test with the above code:

TypeError: Method get Map.prototype.size called on incompatible receiver #<Map>
 ❯ Object.get src/reactive.ts:208:29
    206|       if (Reflect.has(depProps, p)) return Reflect.get(depProps, p)
    207|
    208|       const value = Reflect.get(...args)
       |                             ^
    209|       // For any existing dependency collectors that are active, add this
    210|       // property to their observed properties.

Feature request: add a second parameter for the template function (options object)

Please consider adding a second parameter for the html (template) function. This would be useful for all kinds of initial options, like, for instance,
action: append | prepend | replace | replaceInner | insertBefore | insertAfter | ... - where append is the (current) default action, but replaceInner would probably be a better fit for default action (as most libs/frameworks work this way).

Example code:

...(the html initial structure)
<div class="some-container">
    <span class="some-span">ABC</span>
</div>

... (the script)
html`<span class="some-span">XYZ</span>`(document.querySelector('.some-container'), { 
    action: 'replaceInner' // Replaces the innerHtml of .some-container with the resulting html code
});

...(should result in)
<div class="some-container">
    <span class="some-span">XYZ</span>
</div>

...(instead of)
<div class="some-container">
    <span class="some-span">ABC</span>
    <span class="some-span">XYZ</span>
</div>

Cheers!

Nested template being called unexpectedly

I have a conditionally-included template like this:

const state = reactive({ items: [], index: 0 });

...
${() => hasItem()
            ? html`
                ${() => currentItem().loaded !== false
                    ? html`
                        <img src="${() => currentItem().url}" 
                             @load="${e => onImageLoaded(e, true)}" 
                             @error="${e => onImageLoaded(e, false)}">
                        ${() => currentItem().loaded === null ? loadingAnimation : null}`
                    : html`<div style="color: red;">Item failed to load</div>`}
                <div>Current rating: ${() => currentItem().rating}</div>
            </div>`
            : loadingAnimation}
...

function hasItem() { return state.items.length > state.index; }
function currentItem() { return hasItem() ? state.items[state.index] : null; }

state is a reactive object with an array items and an integer property index. What surprises me is that sometimes I am hitting a javascript error on this line:

<div>Current rating: ${() => currentItem().rating}</div>

The problem is that currentItem() is null. However, I would have expected this not to be called because of the hasItem() conditional. Am I misunderstanding something about how Arrow is supposed to work with conditional templates?

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.