1more
1more copied to clipboard
HTML5 void elements parsing issue
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.
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 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 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.
If this is the case of reformatting on save, then looks like Codesandbox formatter also formats html
literals in XHTML style.
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.
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.
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'}"
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 ofclass="${'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':''}
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
@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.
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.
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)
thenclass=${read(activeRoute) == view ? "show active" : "hide"}
is automatically invalidated.
FYI, I found this document.getElementById("content-placeholder").append(view_foo().n().call().p.templateNode)
- 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 toactiveRoute
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.
-
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 isusePropSubscription
to consume observables from props. There are also brief description and examples in projects Readme. -
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.
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.
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.
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);
}
}
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"));
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.
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()}
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.
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, 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.
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>
`;
}
});
@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.
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 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 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 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 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.
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.
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.
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"));
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?
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.
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
.
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.
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
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 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?
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.
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 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 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 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 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 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)
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 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>`;
};
});
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.
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 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 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.
Thanks @Freak613 That one keeps the structure (in my case it's table) but still misses the font weight & color, background color.
@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.