1more icon indicating copy to clipboard operation
1more copied to clipboard

HTML5 void elements parsing issue

Open Dan-Do opened this issue 3 years ago • 54 comments

Please look at this https://codesandbox.io/s/1more-forked-h62tl

I copy it here

import { html, render } from "1more";
import { box, read } from "1more/box";

let activeRoute = box("/");

render(
  html`<li class="${read(activeRoute) === "/foo" ? "active" : ""}">test-1</li>
    <li class="${read(activeRoute) === "/bar" ? "active" : ""}">test-2</li>`,
  document.getElementById("app")
);

If I remove one of those two li's, it runs fine.

Dan-Do avatar Jun 30 '21 17:06 Dan-Do

Hey, your codesandbox uses 0.1.8 version (probably because I haven't updated it in mine) If you check ver 0.1.15 this example works fine.

Freak613 avatar Jun 30 '21 17:06 Freak613

@Freak613 I have changed the version and it worked ok. Now I updated the template to new complicated one. Please have a look at the link https://codesandbox.io/s/1more-forked-r8qz9?file=/src/index.js Error 'get firstChild' called on an object that does not implement interface Node

Edit: If I save the script, it's automatically formated, somehow magically works. When you test, please remove the spaces and keep original. I copy it here

import { html, render } from "1more";
import { box, read } from "1more/box";

let activeRoute = box("/");

render(
  html`
	<li class="${read(activeRoute)==='/foo'?'active':''}"><a href="/foo"><img src="/images/person.svg" alt="Người" type="image/svg+xml" class="svg-icon"></a></li>
	<li class="${read(activeRoute)==='/bar'?'active':''}"><a href="/bar"><img src="/images/business.svg" alt="Ngành nghề" type="image/svg+xml" class="svg-icon"></a></li>`,
  document.getElementById("app")
);

Dan-Do avatar Jul 01 '21 02:07 Dan-Do

@Dan-Do I see. That was interesting discovery, your <img> elements seems correct HTML5 format but parser failed to parse it, since it was made to parse XHTML where it would be <img />. There is discussion on SO about the differences.

So if you update self closing tags in XHTML format it should work fine.

Freak613 avatar Jul 01 '21 02:07 Freak613

If this is the case of reformatting on save, then looks like Codesandbox formatter also formats html literals in XHTML style.

Freak613 avatar Jul 01 '21 02:07 Freak613

If you are going to "fix" it, I will wait for the release. If not, I am going to change my html. What do you think? BTW, that html template works on Sinuous.

Dan-Do avatar Jul 01 '21 02:07 Dan-Do

For now I'd use XHTML format in your code. I have to review and weight on how fix complicate parser implementation.

Also, I see there is another issue where className "active" leaks into markup instead of applying to element attribute. That is definitely an issue that I have to fix.

Yep, Sinuous uses fork of htm parser and it seems to do better job. I have to review options, since my parser somewhat similar to what htm uses, it just extracts more information from the template structure.

Freak613 avatar Jul 01 '21 02:07 Freak613

Also, I see there is another issue where className "active" leaks into markup instead of applying to element attribute. That is definitely an issue that I have to fix.

Actually I found the problem. There should be no quotes around any attribute bindings. So class=${"my-class"} instead of class="${'my-class'}"

Freak613 avatar Jul 01 '21 03:07 Freak613

Also, I see there is another issue where className "active" leaks into markup instead of applying to element attribute. That is definitely an issue that I have to fix.

Actually I found the problem. There should be no quotes around any attribute bindings. So class=${"my-class"} instead of class="${'my-class'}"

That's true. I cannot do this class="nav-link ${read(activeRoute)=='/'?'active':''}". Instead I have to convert it toclass=${'nav-link'+read(activeRoute)=='/'?' active':''}

Dan-Do avatar Jul 01 '21 11:07 Dan-Do

Yes, as stated in docs, only one binding per attribute allowed. There are different opinions on this topic, but I lean more to the JSX-like semantic, and looking forward to adopt JSX compilation thus achieving TypeScript support.

For your case I would use string literal or some classnames utility tool to merge classnames. This lib currently does not have recommended lib, I probably will take a look on set of default utilities later. One example can be found in TodoMVC implementation: https://github.com/Freak613/1more/blob/main/examples/todomvc/app.js#L169

Freak613 avatar Jul 01 '21 11:07 Freak613

@Dan-Do I see. That was interesting discovery, your <img> elements seems correct HTML5 format but parser failed to parse it, since it was made to parse XHTML where it would be <img />. There is discussion on SO about the differences.

So if you update self closing tags in XHTML format it should work fine.

@Freak613 Is there any "quick and dirty mokey patch" to use HTML5 instead of XHTML? There are so many unknown errors when rendering my templates, so I need that patch to appy only for me, just to test where is the error.

Dan-Do avatar Jul 01 '21 16:07 Dan-Do

Unfortunately it's not that easy. What are other issues you have? Basically, if you have problems you can send new issues here (since I don't currently have other communication channels). The project currently has pretty solid test coverage and I've tried to cover most of the possible issues, so if you spot some problem I would be very curious to take a look at it. Or you can help to build a list of issues experienced by people who coming from native HTML backgrounds.

Freak613 avatar Jul 01 '21 16:07 Freak613

Please look at this link https://codesandbox.io/s/1more-forked-pu42j?file=/src/index.js

  • How to append a component to an element document.getElementById("content-placeholder").append(html`${view_foo()}`) (instead of render & mount)?
  • Is it possible to use observerable outside of component, ex when execute write(view, activeRoute) then class=${read(activeRoute) == view ? "show active" : "hide"} is automatically invalidated.

FYI, I found this document.getElementById("content-placeholder").append(view_foo().n().call().p.templateNode)

Dan-Do avatar Jul 02 '21 11:07 Dan-Do

  1. This lib is closer to VDOM than to native HTML, so html does not return dom node, but TemplateNode, that can be rendered into given container. Renderer designed to own given container, i.e. you can not mix native dom nodes once you rendered into it. As I see that you trying to build a router I would setup subscription to activeRoute box and take component from predefined map like so:
const Routes = {
  foo: Foo,
  bar: Bar,
};

const activeRoute = box("/");

const Bar = component(c => {
  const isActive = useSubscription(
    c,
    activeRoute,
    activeRoute => activeRoute === "bar",
  );

  return () => html`
    <div class=${isActive() ? "show active" : "hide"}>
      this is bar component
    </div>
  `;
});
// same with Foo view

const App = component(c => {
  const getRoute = useSubscription(
    c,
    activeRoute,
  );

  return () => Routes[getRoute()];
});

render(App(), document.getElementById("content-placeholder"));

function loadView(view) {
  write(view, activeRoute);
}

OR if you need to handle many views in array-like

const Routes = {
  foo: Foo,
  bar: Bar,
};

const loadedViews = box([]);

const Bar = component(c => {
  const isActive = useSubscription(
    c,
    loadedViews,
    loadedViews => loadedViews.includes("bar"),
  );

  return () => html`
    <div class=${isActive() ? "show active" : "hide"}>
      this is bar component
    </div>
  `;
});
// same with Foo view

const App = component(c => {
  const getViews = useSubscription(
    c,
    loadedViews,
  );

  return () => getViews().map(view => Routes[view]);
});

render(App(), document.getElementById("content-placeholder"));

function loadView(view) {
  write([...read(loadedViews), view], loadedViews);
}

You can take a look on more extended box and subscription usage in optimized TodoMVC example. But keep in mind that this project is at early development stage, so API could change, thus I would advice to not use this library for anything important apart pet projects.

  1. You can take a look on above-mentioned TodoMVC example on how to use box subscriptions. useSubscription is suitable when your subscription does not coming from component props, otherwise there is usePropSubscription to consume observables from props. There are also brief description and examples in projects Readme.

  2. Nodes internals hold templates, but that's not what you need. It's only static part of template and it does not handle any dynamic insertions. Also, doing it this way most probably will make original TemplateNode unusable, since append will move template contents in whatever target you specified.

Freak613 avatar Jul 02 '21 13:07 Freak613

I just tested 1. and found that it does not keep the state. (updated https://codesandbox.io/s/1more-forked-pu42j)

const Foo = component((c) => {
  let view = "view_foo";
  const isActive = useSubscription(
    c,
    activeRoute,
    activeRoute => activeRoute === view,
  );
  return () => {
    return html`<div class=${isActive() ? "show active" : "hide"}>
      this is foo component
      <input type="text" name="foo" />
    </div>`;
  };
});

When you enter text on the input and switch between two components, the value is reset.

Basically I want to keep all states, so it's better to keep the rendered html and just show/hide it. That's why I need to append the component (only load when a menu-item is clicked by user), not render it which overwrites the root node. Now I am testing 2.

Dan-Do avatar Jul 02 '21 15:07 Dan-Do

When you enter text on the input and switch between two components, the value is reset.

This is expected behavior, when you switch component, you effectively unmount previous one and mount new one. If you need to preserve the state during unmounts, you can use global state or lift it to the parent component. Or you can use styles to hide them visually while keeping in the DOM.

I'd advice for you to take a look on how popular VDOM libraries are used, like React, to lower head start with this one. Once you get familiar with it, things in here will get a lot more easier to understand.

Freak613 avatar Jul 02 '21 15:07 Freak613

Thank you for the advice. I just need a simple solution, so I am going with this for now.

function loadView(view) {
  write(view, activeRoute);
  if (!loadedViews.includes(view)) {
    document
      .querySelector("#content-placeholder")
      .append(document.createElement("div"));
    if (view === "view_foo")
      render(
        Foo(),
        document.getElementById("content-placeholder").lastElementChild
      );
    else if (view === "view_bar")
      render(
        Bar(),
        document.getElementById("content-placeholder").lastElementChild
      );
    loadedViews.push(view);
  }
}

Dan-Do avatar Jul 02 '21 16:07 Dan-Do

What's wrong with this? I can't access the arguments

const App = component((c) => {
  let foo = "foo";
  return (view, pages, currentPage, callback) => {
    return html`<nav aria-label="Page navigation" class="w-100 mt-3">
		<ul class="pagination justify-content-center">${pages.map(page => html`
			<li class=${'page-item'+(page==currentPage ? ' active' : '')}>
				<a class="page-link" href=${view+'/?p='+page} onclick=${(e)=>{e.preventDefault();callback(page);}}>${page}</a>
			</li>`)}
		</ul>
	</nav>`;
  };
});

render(App("/foo",[1,2,3],2,alert), document.getElementById("app"));
#can't access property "map", pages is undefined

Edit: It seems we must pass a single argument which is an object.

const App = component((c) => {
  let foo = "foo";
  return ({view, pages, currentPage, callback}) => {
    return html`<nav aria-label="Page navigation" class="w-100 mt-3">
		<ul class="pagination justify-content-center">${pages.map(page => html`
			<li class=${'page-item'+(page==currentPage ? ' active' : '')}>
				<a class="page-link" href=${view+'/?p='+page} onclick=${(e)=>{e.preventDefault();callback(page);}}>${page}</a>
			</li>`)}
		</ul>
	</nav>`;
  };
});

render(App({view: "/foo", pages: [1,2,3], currentPage: 2, callback: alert}), document.getElementById("app"));

Dan-Do avatar Jul 04 '21 09:07 Dan-Do

Yes, components accept only one argument that can be a props object like in vdom libs.

And yes, you can use preventDefault, it should work properly. Keep in mind that all event handlers are in bubble phase, i.e. from bottom-to-top.

Freak613 avatar Jul 04 '21 16:07 Freak613

Can youexplain why the onclick does not work in this example

const App = component((c) => {
  const error = box('');
  const getError = useSubscription(c, error);
  function ReloadState() {alert('ok')}
  return () => {
    return html`<div role="alert" class='notification'>
      <i class="button is-ghost reset is-size-4 px-4 bi-arrow-repeat" onclick=${ReloadState}></i>
      ${getError()}
    </div>`;
  }
});

It works when I remove the ${getError()}

Dan-Do avatar Jul 13 '21 04:07 Dan-Do

Yeah, this is indeed a bug. At the moment, you can get around it wrapping getError in additional element, span for example. I'll fix it a bit later.

Freak613 avatar Jul 13 '21 05:07 Freak613

Could you please help me check this? Uncaught TypeError: can't access property "a", prev.i is undefined

const App = component((c) => {
  const vatInvoice = {id:"vat-test"};
  const showModal = box(true);
  const isShown = useSubscription(c, showModal);
  const ok = box(false);
  const isOk = useSubscription(c, ok);
  return () => {
    return html`
  <div class=${'modal'+(isShown() ? ' is-active' : '')} id="dlg-vat-invoice">
    <div class="modal-background" onclick=${() => write(false, showModal)}></div>
    <form class=${'modal-card modal-lg'+(isOk ? '' : ' validated')} role="form">
      ${vatInvoice.id && html`<input type="hidden" name="_key" value=${vatInvoice.id}/>`}
      <header class="modal-card-head">
        <h5 class="modal-card-title">${(vatInvoice.id ? 'Edit' : 'Add') + ' invoice'}</h5>
        <span class="delete is-large" aria-label="close" onclick=${() => write(false, showModal)}></span>
      </header>
    </form>
  </div>`;
  }
});

It doesn't raise error if I do one of 2 following:

  • remove the line <h5 class="modal-card-title">${(vatInvoice.id ? 'Edit' : 'Add') + ' invoice'}</h5>
  • or just keep this
return () => {
    return html`
<h5 class="modal-card-title">${(vatInvoice.id ? 'Edit' : 'Add') + ' invoice'}</h5>
<span class="delete is-large" aria-label="close" onclick=${() => write(false, showModal)}></span>
`;

Dan-Do avatar Jul 19 '21 11:07 Dan-Do

@Dan-Do, sorry, I can't reproduce the issue. Can you provide codesandbox for it? One problem I noticed is where you accessed isOk whereas it should be isOk(). Are you talking about onclick event or just rendering? If onclick, I'm curious, are you testing sending events from code? Because elements that do have event handlers do not have any content, so they're not clickable from the actual page.

Freak613 avatar Jul 19 '21 21:07 Freak613

Also, for local state, I would advice to try use simple primitives. In this case only thing is that you have to add invalidate calls to your event handlers, to trigger component update (or do not call it if you don't need to rerender component). But it can reduce component internal code size.

const App = component((c) => {
  const vatInvoice = {id:"vat-test"};

  let showModal = true;
  let ok = false;

  const closeModal = () => {
    showModal = false;
    invalidate(c);
  };

  return () => {
    return html`
      <div class=${'modal'+(showModal ? ' is-active' : '')} id="dlg-vat-invoice">
        <div class="modal-background" onclick=${closeModal}></div>
        <form class=${'modal-card modal-lg'+(ok ? '' : ' validated')} role="form">
          ${vatInvoice.id && html`<input type="hidden" name="_key" value=${vatInvoice.id}/>`}
          <header class="modal-card-head">
            <h5 class="modal-card-title">${(vatInvoice.id ? 'Edit' : 'Add') + ' invoice'}</h5>
            <span class="delete is-large" aria-label="close" onclick=${closeModal}></span>
          </header>
        </form>
      </div>
    `;
  }
});

Freak613 avatar Jul 19 '21 21:07 Freak613

@Dan-Do I see, you were talking about event handler calls. This is also an issue. I'm going to check both of these issues on this week.

Freak613 avatar Jul 19 '21 21:07 Freak613

Sorry I was not clear enough. Yes, it was about click event. Here it is https://codesandbox.io/s/1more-forked-8uqd0?file=/src/index.js Though the span <span class="delete is-large" aria-label="close" onclick=${()=>write(false, showModal)}></span> does not have any actual content, but it has ::before ::after content. I am looking forward to the fix :)

Dan-Do avatar Jul 20 '21 03:07 Dan-Do

@Dan-Do okay, fixes are live, it's version 0.1.16.

For this example now click works properly: https://github.com/Freak613/1more/issues/7#issuecomment-878765612

And for this, as I understand the problem was that it throws an error when clicking "Edit invoice", and there are no event handlers to work, just this error. So I fixed the problem that caused this and it should work now without issue: https://github.com/Freak613/1more/issues/7#issuecomment-882474707

Freak613 avatar Aug 01 '21 08:08 Freak613

@Freak613 I have another error Uncaught TypeError: 'get nextSibling' called on an object that does not implement interface Node. on this https://codesandbox.io/s/1more-forked-d440c?file=/src/index.js Try to click outside the button.

const App = component((c) => {
  const items = [{content:'test'}];
  return () => {
    return html`
    <form class='modal-card modal-lg' onsubmit=${()=>event.preventDefault()} role="form">
      <section class="modal-card-body">
        ${items.map((item,idx) => html`
        <div class="columns mb-2">
          <label class="vat-label is-flex label">
            <button class=${'delete is-medium'+(items.length==1?' is-hidden':'')} onclick=${(e)=>console.log(e)}></button>
          </label>
        </div>`)}
        <div class="columns mb-2">
          <label class="label"><i class="button is-light bi-plus reset" onclick=${(e)=>console.log(e)}></i></label>
        </div>
      </section>
    </form>`;
  }
});

Dan-Do avatar Aug 03 '21 08:08 Dan-Do

@Dan-Do okay, apparently this one is related to previous fix. Fixed this one also in version 0.1.18, so all 3 last cases should work.

Freak613 avatar Aug 03 '21 11:08 Freak613

@Freak613 I don't know if I am doing wrong, please help me check the below html. The error is can't access property "t", l is undefined

const App = component((c) => {
  const showModal = usePropSubscription(c);
  const items = [{content:'test'}];
  return (vatInvoice) => {
    return html`<button type="button" class="button is-primary ml-4" onclick=${()=>vatInvoice.showModal=true}>show/hide</button>
    <form class=${'modal'+(showModal(vatInvoice.showModal)?' is-active':'')} onsubmit=${()=>event.preventDefault()} role="form">
      <section class="modal-card-body">
        ${items.map((item,idx) => html`
        <div class="columns mb-2">
          <label class="vat-label is-flex label">
            <button class=${'delete is-medium'+(items.length==1?' is-hidden':'')} onclick=${(e)=>console.log(e)}></button>
          </label>
        </div>`)}
        <div class="columns mb-2">
          <label class="label"><i class="button is-light bi-plus reset" onclick=${(e)=>console.log(e)}></i></label>
        </div>
      </section>
    </form>`;
  }
});

const vatInvoice = {date:'2021-08-03', buyer:'test', showModal: false};
render(App(vatInvoice), document.getElementById("content-placeholder"));

Regarding this example, there is a problem with using usePropSubscription. It expects box coming from props, not just the plain value. So you can either put showModal: box(false) into your vatInvoice model or rewrite the App to have initialization consuming plain value from props and creating local box observable.

But again, I'd advice to start the code from using plain values + invalidate calls once you need component to rerender and see how far you can go. Current observable implementation was just last minute thought, so that's why it may not have good reasoning in its API and will be either dropped or replaced with something simpler to use.

Freak613 avatar Aug 03 '21 12:08 Freak613

There was another discussion with plans regarding observable replacement: https://github.com/Freak613/1more/issues/3#issuecomment-844507661

I'm still targeting the plan and have all the working code. The only thing I got struggled with is API design, what taste of observability I would prefer to wire with this lib (and any other lib), because it's not enough to drop raw code without shaping it into some consumable form. So I keep doing my research.

Freak613 avatar Aug 03 '21 13:08 Freak613

Thanks @Freak613, I knew it was wrong so just deleted my comment, but you're so fast :-) I will get back if there is any error.

Dan-Do avatar Aug 03 '21 13:08 Dan-Do

I am trying the plain values & invalidate but it runs into infinity loop on this scenario.

const App = component((c) => {
  let error = undefined;
  const items = [{content:'test'}];
  function getData() {error = 'fake'; invalidate(c);}
  return (vatInvoice) => {
    getData();
    return html`<button type="button" class="button is-primary ml-4" onclick=${()=>{vatInvoice.showModal=!vatInvoice.showModal;error='test';invalidate(c);}}>show/hide</button>
    <form class=${'modal'+(vatInvoice.showModal?'':' is-hidden')} onsubmit=${()=>event.preventDefault()} role="form">
      <section class="modal-card-body">
        ${items.map((item,idx) => html`
        <div class="columns mb-2">
          <label class="vat-label is-flex label">
            <button class=${'delete is-medium'+(items.length==1?' is-hidden':'')} onclick=${(e)=>console.log(e)}></button>
          </label>
        </div>`)}
        <div class="columns mb-2">
          <label class="label"><i class="button is-light bi-plus reset" onclick=${(e)=>console.log(e)}></i>${error}</label>
        </div>
      </section>
    </form>`;
  }
});
const vatInvoice = {date:'2021-08-03', buyer:'test', showModal: false};

render(App(vatInvoice), document.getElementById("content-placeholder"));

Dan-Do avatar Aug 05 '21 07:08 Dan-Do

The function that you return in component is its re-render function. So what’s happening is you asking to rerender the component through getData each time it’s rendering.

Why you calling it from render function?

Freak613 avatar Aug 05 '21 08:08 Freak613

If your intent is to perform some setup on component first render, you can isolate this block of code by some local flag. However there is still no need to call invalidate in this case. invalidate is better to call from event handlers or when something happens in the system, when it’s not rendering.

Freak613 avatar Aug 05 '21 09:08 Freak613

I want to change the state of some variables when fetching data, for example isLoading (show a spinner), reset the error text... I think I must use a variable called isRendered to check when there is first run (component is not yet rendered). By the way, it does not happen when I use useSubscription and usePropSubscription.

Dan-Do avatar Aug 05 '21 09:08 Dan-Do

Do you want to fetch data each time component rerendered?

If you need it on demand it can be:

const App = component(c => {
  let isLoading = false;
  let data;
  let errorText;

  const fetchData = async () => {
    isLoading = true;
    invalidate(c);

    data = await Promise.resolve(1);

    isLoading = false;
    invalidate(c);
  };

  const resetError = () => {
    errorText = null;
    invalidate(c);
  };

  return () => html`
    <button onclick=${fetchData}>Fetch Data</button>
    <button onclick=${resetError}>Reset Error</button>
    ${data}
    ${errorText}
  `;
});

If you need it only once on first render:

const App = component(c => {
  let isLoading = false;
  let data;
  let errorText;
  let firstRender = true;

  const fetchData = async () => {
    // No need to invalidate, since it's called as part of first render.
    isLoading = true;

    data = await Promise.resolve(1);

    isLoading = false;
    // This one still needed, since Promise will resolve after component rendered.
    invalidate(c);
  };

  const resetError = () => {
    errorText = null;
  };

  const setup = () => {
    fetchData();
    resetError();
    firstRender = false;
  }

  return () => {
    if (firstRender) setup();

    return html`
      ${data}
      ${errorText}
    `;
  }
});

And if you really need to fetch data on each render:

const App = component(c => {
  let isLoading = false;
  let data;

  const fetchData = async () => {
    // No need to invalidate since it's called as part of each render.
    isLoading = true;

    data = await Promise.resolve(1);

    isLoading = false;
    invalidate(c);
  };

  return () => {
    fetchData();

    return html`
      ${data}
      ${isLoading}
    `;
  }
});

However this one will be looped, just not immediately given that it awaits Promise resolve. But still the process is infinite by idea.

Freak613 avatar Aug 05 '21 10:08 Freak613

Does the invalidate update the child component without using usePropSubscription? Please help me look at this example. https://codesandbox.io/s/1more-forked-vbc3k?file=/src/index.js

Dan-Do avatar Aug 05 '21 13:08 Dan-Do

Child components will rerender whenever parent rerendered or they are invalidated themselves. The only exception that if you give it the same properties as in previous render, it will not re-render. This is performance optimization (so called shouldComponentUpdate from React) to avoid doing same work if it can be safely memoized. Properties object is compared as a whole using strict equality (===).

So in your case Modal may not be rerendered if its properties are the same object instance. To overcome that you can use immutable patterns, like creating new object instance when you need to modify it's data:

vatInvoice = {...vatInvoice, isShown: !vatInvoice.isShown};

Or you can create new properties for Modal each time parent updates without optimization:

`${Modal({ ...vatInvoice })}`
// OR
`${Modal({ isShown: vatInvoice.isShown })}`
// OR redesign Modal props to be just one value
`${Modal(vatInvoice.isShown)}`

Freak613 avatar Aug 05 '21 20:08 Freak613

@Freak613 I think the issue is solved now, you can close it.

BTW, I have some questions, just let me know if you want to open issue cause it isn't real issue.

I want to build a tiny component that replace the search text with formated results. Something like this


const App = component((c) => {
  const CompHightLight = component(c => {
    return ({needle, haystack}) => {
      return html`${haystack.replace(needle, `<li class='has-text-warning'>${needle}</li>`)}`;
      };
  });
  return () => {
    let data = ["this is a test string", "my test is ok!"];
    return html`<ul>
      ${data.map((item) => CompHightLight({needle:"test", haystack:item}))}
    </ul>`;
  }
});

But it just show the plain html. I know I did it wrong but don't know how to fix. Could you please give me some hints?

Dan-Do avatar Aug 07 '21 05:08 Dan-Do

From what I understand you need to wrap parts of the original string into some html element. So basically you need to split the string into parts and map them into plain text or html element:

const CompHightLight = component(() => {
  return ({needle, haystack}) => {
    const regex = new RegExp(`(${needle})`);
    const parts = haystack.split(regex); // ["this is a ", "test", " string"]

    // It's possible to return strings and arrays directly from render function.
    return parts.map(part => {
      if (part === needle) {
        return html`<li class='has-text-warning'>${needle}</li>`;
      }
      return part;
    });
  };
});

And I will keep this issue open, to get back to HTML5 elements parsing later. whether it is fixable or not.

Freak613 avatar Aug 07 '21 05:08 Freak613

Could you help me look at this? The error is TypeError: props.p is undefined

const App = component((c) => {
  let error = undefined;
  function Save() {
    error = undefined; //reset error
    invalidate(c);
    error = {foo: "foo"};
    error = {bar: "bar"};
    invalidate(c);
  }
  return (vatInvoice) => {
    return html`<button onclick=${Save}>Submit</button>
    <ul>
      <div role="alert" class=${typeof error==='string'?'notification is-danger':'is-invisible'}>${error}</div>
      <li class='error'>${error && error.foo}</li>
      <li class='error'>${error && error.bar}</li>
    </ul>`;
  }
});

Dan-Do avatar Aug 07 '21 10:08 Dan-Do

@Dan-Do the problem is most probably that you assign an object to the error and then rendering it inside div.alert. Library does not support rendering objects, so try to convert the error into something renderable. In your case you can try to add checking same as with classes, if it's not a string then render null or empty string.

Freak613 avatar Aug 07 '21 10:08 Freak613

@Freak613 It seems the value undefined of an input does not updated. Here is the example:

let vatInvoice = {date:'2021-08-03', buyer:'test', amount: 10};
const App = component((c) => {
  function Save() {
    vatInvoice = {date:Date.now(), buyer:undefined, amount: null};
    invalidate(c);
  }
  return () => {
    return html`<button onclick=${Save}>Submit</button>
    <ul>
      <input value=${vatInvoice.date} />
      <input value=${vatInvoice.buyer} />
      <input value=${vatInvoice.amount} />
    </ul>`;
  }
});

When you click Save, the date is changed but buyer and amount still keep the old values.

Dan-Do avatar Aug 07 '21 16:08 Dan-Do

@Dan-Do yes, when assigning null or undefined to native element attribute, library performs removal of the HTML attribute from the element. And in this case, removal of value attribute of input element does not result in resetting its current value. I would advice to use undefined and null when you really need to remove attribute, in other cases and in case of input fields it's better to use empty string to reset actual value of the element.

Freak613 avatar Aug 08 '21 00:08 Freak613

@Freak613 I am using ClipboardJS to manage the copy on each rendered item. It runs ok except after the first render when items are not attached to the DOM. Do we have some thing called afterRendered just like useUnmount?

Dan-Do avatar Aug 08 '21 12:08 Dan-Do

@Dan-Do no, currently lib does not have hook for after mounting and does not have refs to work with native elements. I'm planning to implement it. But given that render is synchronous, it may be possible to temporarily solve this problem with using Promise.resolve() to schedule execution after completion of current stack frame:

const setup = async () => {
  await Promise.resolve();
  console.log('Possibly mounted');
};

Can you try this approach and see if it will work? This code is very close to real hook, the only difference is that final code will have predictable order of execution (from top to bottom or from bottom to top of the component tree)

Freak613 avatar Aug 08 '21 20:08 Freak613

Thanks @Freak613 As I am not good at Promise, so I use this "monkey patch" and it works ok.

function LoadView(e) {
  ...
  if (!loadedViews().includes(route)) {
    loadedViews([...loadedViews(), route]);
    if (route === "/product/vat-invoice")
      setTimeout(() => document.querySelectorAll('[data-clipboard-target]').forEach((item) =>
        new ClipboardJS(item, {target: () => document.querySelector(item.attributes['data-clipboard-target'].value)})), 1000);
  }
}

const MyComponent = component(c => {
  function fetchData() {
    fetch(...).then( (data) => {
      // assign data;
      invalidate(c);
      // the below code does not run on first render because items are not attached to DOM yet
      document.querySelectorAll('[data-clipboard-target]').forEach((item) =>
        new ClipboardJS(item, {target: () => document.querySelector(item.attributes['data-clipboard-target'].value)}));
    });    
  return () => {
    return html`...`;
  };
});

Just only one funny thing is, when new data is fetched, although the item is refreshed with new content, but the selected mark is still highlight. But it's minor thing, I can live with that. BTW, Could you give me pseudo code of your Promise approach?

Dan-Do avatar Aug 09 '21 01:08 Dan-Do

@Dan-Do yes, I already put example snippet in my previous comment. To expand it further:

const App = component((c) => {
  let isRendered = false;
  let firstRun = true;

  const setup = async () => {
    firstRun = false;

    // Wait for next stackframe
    await Promise.resolve();

    // App is rendered. invalidate to display flag in UI.
    isRendered = true;
    invalidate(c);
  };

  return () => {
    if (firstRun) setup();

    return html`<div>${isRendered}</div>`;
  };
});

Freak613 avatar Aug 09 '21 01:08 Freak613

But again, to perfectly integrate third-party vanillaJS libs with 1more, it lacks abovementioned functionality of refs and mount hooks. And I'm not sure is there real need to use ClipboardJS, because code snippet to copy text into clipboard is pretty straightforward and most probably can be found on StackOverflow or MDN. And that could be implemented with tools that already there in the lib, just have an input and the button:

const copyToClipboard = text => {
  // some code from MDN
};

const Item = component((c) => {
  let value;

  const onChange = e => {
    value = e.target.value;
    invalidate(c);
  };

  return () => {
    return html`
      <input value=${value} oninput=${onChange} />
      <button onclick=${() => copyToClipboard(value)}>Copy</button>
    `;
  };
});

ClipboardJS seems designed for environment without much JS on the client side, where you have static HTML and you don't want to write more than one line of code to attach event handlers to targets. Pretty much like with jQuery.

Freak613 avatar Aug 09 '21 01:08 Freak613

Or maybe not so straightforward, but still smaller and more portable than separate lib: https://www.30secondsofcode.org/js/s/copy-to-clipboard This website has lot of useful snippets for everyday.

That lib is 3.3Kb min+gzip because it does a lot more (and they have their own event delegation and code to attach handlers), while this snippet is very lightweight and should not be more than 500b. https://bundlephobia.com/package/[email protected]

Freak613 avatar Aug 09 '21 01:08 Freak613

@Freak613 I just looked at the snippet at link https://www.30secondsofcode.org/js/s/copy-to-clipboard It can only copies the plain html text, while the https://bundlephobia.com/package/[email protected] (actually ClipboardJS which I am using) keeps formatted when I paste to GMail.

Dan-Do avatar Aug 09 '21 03:08 Dan-Do

@Dan-Do sure, you can look up solutions in google that match your needs. For example this one seems to have rich text copy to clipboard and just 10 lines of code: https://stackoverflow.com/a/50067769 So you only have to be able to get innerHTML of target element to paste into clipboard. clipboardData seems to be supported in all modern browsers.

PS: However it does not work in Safari for some reason. Anyway it's a matter of time to spend and personal preference. At current moment 1more does not have support of integration with third-party libs, but will be added later.

Freak613 avatar Aug 09 '21 03:08 Freak613

Thanks @Freak613 That one keeps the structure (in my case it's table) but still misses the font weight & color, background color.

Dan-Do avatar Aug 09 '21 03:08 Dan-Do

@Dan-Do I see. So probably it does something on top of plain copying to gather all styling, that could make sense of its size. For now you can use method with Promise + selector matching to wire it with rendered view.

Freak613 avatar Aug 09 '21 03:08 Freak613