Sapper/Svelte - need #html included content to invoke component - javascript

I'm building a site using sapper that uses the [slug].svelte convention for individual blog entries. The blog content comes out of a (simulated) database, and contains html.
The html is included at the bottom, like so:
...
<div class="content">
{#html post.html}
</div>
...
All well and good, it renders the html just peachy.
However, consider the following:
In the [slug].svelte file:
import AComponent from '../../components/AComponent.svelte'
And, in the included html rendered inside the {#html post.html}:
<p>yada yada yada</p>
<AComponent prop="data" />
<p>More yada yada yada...</p>
AComponent doesn't get instantiated or invoked.
Is there a way to make this happen? Or am I trying to do something not possible?
(I know the component is ok - it's been tested in another file with complete html.)
Thanx

I don't believe that's possible with the #html directive alone.
From the docs:
The expression should be valid standalone HTML — {#html "<div>"}content{#html "</div>"} will not work, because is not valid HTML.
Since <AComponent prop="data" /> is a Svelte component and not standalone HTML, it won't instantiate itself when using #html.
However, you could wrangle together a solution using <svelte:component> to render a component from string content dynamically.
Here's a quick proof of concept in the Svelte REPL. There's a bunch of edge cases left uncovered, but it shows that it's possible. I've also pasted the code below.
<script>
import ComponentA from './ComponentA.svelte';
import ComponentB from './ComponentB.svelte';
let raw = `<p>yada yada yada</p>
<ComponentA name="testing" />
<p>More yada yada yada...</p>`;
$: rawLines = raw.split('\n');
function getComponent(line) {
const componentName = line.split(' ')[0].substring(1);
switch (componentName) {
case 'ComponentA':
return ComponentA;
case 'ComponentB':
return ComponentB;
}
return null;
}
function getComponentProps(line) {
const props = line.split(' ').slice(1, -1);
const kvPairs = props.map(p => p.replaceAll('"', '').split('='));
return Object.fromEntries(kvPairs);
}
</script>
<textarea bind:value={raw} />
{#each rawLines as line}
{#if line[1] === line[1].toUpperCase()}
<svelte:component this={getComponent(line)} {...getComponentProps(line)}></svelte:component>
{:else}
{#html line}
{/if}
{/each}

Related

Parse and Render external HTML in React component

I'm writing a React-based application where one of the components receives its HTML content as a string field in props. This content is returned by an API call.
I need to:
Render this content as a standard HTML (i.e. with the styles applied)
Parse the content to see if the sections within the content have "accept-comments" tag and show a "Comment" button beside the section
For example, if I receive the HTML below, I should show the "Comment" button beside section with id "s101".
<html>
<head/>
<body>
<div id="content">
<section id="s101" accept-comments="true">Some text that needs comments</section>
<section id="s102">Some text that doesn't need comments</section>
</div>
</body>
</html>
Questions:
What would be the most efficient way to parse and render the HTML as the content can get a bit large, close to 1MB at times?
How can I ensure that React does not re-render this component as it will not be updated? I'd assume always return "false" from shouldComponentUpdate().
Things I've tried:
Render the HTML with "dangerouslySetInnerHTML" or "react-html-parser". With this option, cannot parse the "accept-comments" sections.
Use DOMParser().parseFromString to parse the content. How do I render its output in a React component as HTML? Will this be efficient with 1MB+ content?
This answer comes from Chris G's code in the comments. I used the code with different sizes of documents and it works well. Thanks Chris G!
Posting the code here in case the link link in the comments breaks.
The solution uses DOMParser to parse the HTML content provided by the API call and scans it to find the content that should include the "Comment" button. Here are the relevant parts.
import React from "react";
import { render } from "react-dom";
const HTML =
"<div><section but='yes'>Section 1</section><section>Section 2</section></div>";
class DOMTest extends React.Component {
constructor(props) {
super(props);
const doc = new DOMParser().parseFromString(HTML, "application/xml");
const htmlSections = doc.childNodes[0].childNodes;
this.sections = Object.keys(htmlSections).map((key, i) => {
let el = htmlSections[key];
let contents = [<p>{el.innerHTML}</p>];
if (el.hasAttribute("but")) contents.push(<button>Comment</button>);
return <div key={i}>{contents}</div>;
});
}
render() {
return <div>{this.sections}</div>;
}
}
const App = () => (
<div>
<DOMTest />
</div>
);
render(<App />, document.getElementById("root"));

What is an elegant way to initialize multiple react-live components from a static website?

I'm currently creating a react component library that i want to provide as an npm package. I also want to provide a documentation that features fancy live rendering of the available components on e.g. github pages.
For the live editing feature i'm planning to use react-live which provides multiple react components to display a live editor and a preview. See an example from the styled-components docs how this looks like.
The react-live component accepts a string code containing the initial code that should be displayed in the editor and a list of components scope that can be used inside the live editor.
Now i want to use gohugo or a similar static site generator to deploy my documentation. I thought i could maybe provide a <div> inside my static site that has a special class react-live-demo and i will get these containers with document.getElementsByClassName('react-live-demo'), loop over them and render the react-live component into it.
I created a code snippet that shows a little example:
const {LiveProvider, LiveEditor, LiveError, LivePreview} = window['react-live'];
// a random component that i want to render in the live editor
const MyComponent = () => (
<div>
<h1>react live demo</h1>
<p>This is a component from the script tag.</p>
</div>
);
// a wrapper for the react-live editor
const Editor = ({code, scope}) => (
<LiveProvider code={code} scope={scope}>
<div className="live-example row">
<div className="col-xs">
<LiveEditor />
<LiveError />
</div>
<div className="col-xs">
<LivePreview />
</div>
</div>
</LiveProvider>
);
// get all containers that have the initial code as innerHTML
const examples = document.querySelectorAll('script[data-name="react-live"]');
examples.forEach(example => {
// insert a new div before the script tag
const container = document.createElement('div');
example.parentNode.insertBefore(container, example.nextSibling);
// render the editor with the code from the script tag
const code = example.innerHTML.trim();
ReactDOM.render(<Editor code={code} scope={{MyComponent}} />, container);
});
.static-content {
background-color: papayawhip;
}
.live-example {
border: 1px solid orange;
}
.react-live-demo.code {
display: none;
}
.invalid {
color: red;
}
<p class="static-content">HERE IS SOME STATIC HTML CONTENT</p>
<script type="text/html" data-name="react-live">
<div>
<h1>react live demo</h1>
<p>This code is editable.</p>
</div>
</script>
<p class="static-content">SOME MORE STATIC HTML CONTENT</p>
<script type="text/html" data-name="react-live">
<MyComponent />
</script>
<link href="https://cdnjs.cloudflare.com/ajax/libs/flexboxgrid/6.3.1/flexboxgrid.min.css" rel="stylesheet"/>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<script src="https://unpkg.com/react-live#1.7.0/dist/react-live.min.js"></script>
The question is now:
How do embed the code string into my static site so that i can grab it with my js code and pass it to my react-live component?
I thought about <pre> or something like that. Or should i just embed it as inner html and read that? But then it would be displayed for short when the react component hasn't rendered yet. Or should i use some sort of script tag and make it available as globals? The key goal is to make it as easy as possible to add live editing examples to the documentation without touching any javascript code.
However if i pass something like <MyComponent /> as innerHTML this does of course not work as you can see in the code snippet.
Any best practice for that use case appreciated.
EDIT:
Based on the suggestion of #Daniel Alexandrov i edited the code snippet. The solution now reads the innerHTML of script tags with type="text/html set and creates a <div> container to insert the editor. This seems to work quite well. Any more ideas?
In my opinion the best choice is to use the <script> tag with a custom type attribute. Take a look at the Knockout.js template binding which uses type="text/html or the Angular <script type="text/ng-template">
This way the browser will ignore the script tag completely, because it doesn't know how to interpret it.

Using a regular JavaScript library inside a React component

I'm curious what's the best way to use a regular JavaScript library (not written as a React component) inside a React environment.
For example, let's say there's a JavaScript library that embeds a simple widget to my webpage. The instructions are as follows:
Include the loading tag in the header.
Embed the snippet anywhere you want.
In a normal webpage, I would do the following:
<head>
<script src="http://karaoke.com/source.js"></script>
</head>
<body>
<h1>Hello look at my cool widget library</h1>
<karaoke width="600" height="400" />
</body>
How do I achieve the same effect where I have a React component like this?
class MainView extends Component {
render() {
return (
<div>
<h1>I want to show my karaoke widget here, but how?</h1>
</div>
);
}
}
The main purpose of JSX is to feel like HTML. The main purpose of render in a React component is to render that "fake" (virtual) HTML. If your external widget is also a React component, the solution is straightforward:
class MainView extends Component {
render() {
return (
<div>
<h1>Hello look at my cool widget library</h1>
<Widget width="600" height="400" />
</div>
);
}
}
All you have to make sure is that the code above runs after your widget script is loaded (so the Widget component is present). The exact solution to this would depend on your build system.
If your external library is not a React component, then you cannot use a custom widget tag and you must render actual HTML.
The big trick is to not return from render but manually render ourselves after the widget initializes what it needs:
class MainView extends Component {
render() {
// don't render anything
return <div/>;
},
componentDidMount() {
// find the DOM node for this component
const node = ReactDOM.findDOMNode(this);
// widget does stuff
$(node).activateMyCoolWidget();
// start a new React render tree with widget node
ReactDOM.render(<div>{this.props.children}</div>, node);
}
});
Take a look at portals for more details.

React is rendering md (remarkable) as a string

Below is a dummied down version of the component I'm pulling into another component.
import React from "react"
var Remarkable = require('remarkable');
var md = new Remarkable();
const Info = (props) => {
return (
<div className="pop">
<div>
<h1>{props.title}</h1>
{md.render('# Remarkable rulezz!')}
</div>
</div>
)
}
export default Info;
On the page it is currently rendering.
(the title passed as props, as a h1, and...)
<h1># Remarkable rulezz!</h1>
So it's literally rendering out the whole thing as a string, when I want it to behave like html.
How do I do this? Or have I missed the whole point of md?
Thanks
If you for some reason need to render the html as-is using react you need to use dangerouslySetInnerHTML and wrap the content with any tag (div or span or something else)
<div dangerouslySetInnerHTML={{ __html: md.render('# Remarkable rulezz!') }} />
References:
https://facebook.github.io/react/tips/dangerously-set-inner-html.html

How to enhance a server side generated page with Aurelia.io?

I'm writing an app with some parts as SPA and some pages generated on server side for SEO. I've chosen Aurelia.io framework and I use enhance method to enable custom elements on my pages. But I can't find the best way to use aurelia specific template directives and interpolation on my server side page. Let's start with an exemple.
All of my pages contains a dynamic header. This header will be a custom element named my-cool-header. This header will load authentified user and display its name, or, if no user is currently authentified, a link to the signin will be displayed. The body of the page will be generated on server side and cached. So, we'll have something like that :
<html>
<body>
<my-cool-header>
<img src="logo.png">
<div
show.bind="user">${user.name}</div>
<div
show.bind="!user">Sign-in</div>
</my-cool-header>
<div>Cachabled content</div>
</body>
</html>
Then, my header will by defined by :
import {UserService} from './user';
import {inject} from 'aurelia-framework';
#inject(UserService)
export class MyCoolHeader {
constructor(userService) {
this.userService = userService;
}
async attached() {
this.user = await this.userService.get();
}
}
With the following template :
<template>
<content></content>
</template>
And this bootstrap script :
export function configure(aurelia) {
aurelia.use
.standardConfiguration()
.developmentLogging()
.globalResources('my-cool-header');
aurelia.start().then(a => a.enhance(document.body));
}
In this configuration, the custom element is well loaded and instanciated. But, I can't access the viewModel of the node inside the <content> node. So, all the interpolation (${user.name}) and attributes (show.bind) are ignored. If I include a custom-element in my content template, it will be loaded only if it is declared as global in the bootstrap : the` tag is ignored.
I've found a workaround to be able to change the viewModel after reading the doc by setting a custom viewModel to enhance method and then, injecting it to my custom element class. Something like :
import {MainData} from './main-data';
export function configure(aurelia) {
const mainData = aurelia.container.get(MainData);
aurelia.use
.standardConfiguration()
.developmentLogging()
.globalResources('my-cool-header');
aurelia.start().then(a => a.enhance(mainData, document.body));
}
Custom element:
import {UserService} from './user';
import {inject} from 'aurelia-framework';
import {MainData} from './main-data';
#inject(UserService, MainData)
export class MyCustomElement {
constructor(userService, mainData) {
this.userService = userService;
this.mainData = mainData;
}
async attached() {
this.mainData.user = await this.userService.get();
}
}
And finally, if I change my template like that, it will work :
<html>
<body>
<my-cool-header
user.bind="user">
<img src="logo.png">
<div
show.bind="user">${user.name}</div>
<div
show.bind="!user">Sign-in</div>
</my-cool-header>
<div>Cachabled content</div>
</body>
</html>
I can't believe it is the right way to do because it's ugly and it does not resolve the problem of <require> tag. So my question is : What is the best way to do ?
Thanks to your clues, I found the solution!
Custom element need to construct its own template:
import {processContent, noView} from 'aurelia-framework';
#processContent(function(viewCompiler, viewResources, element, instruction) {
instruction.viewFactory = viewCompiler.compile(`<template>${element.innerHTML}</template>`, viewResources, instruction);
element.innerHTML = '';
return false;
})
#noView
export class MyCustomElement {
attached() {
this.world = 'World!';
this.display = true;
}
}
Then, in my view from server, we can interpolate and require custom elements!
<body>
<my-custom-element>
<require="./other-custom-element"></require>
<p
if.bind="display">Hello ${world}</p>
<other-custom-element></other-custom-element>
</my-custom-element>
</body>
I've wrote a decorator to help creating this kind of enhanced custom elements : https://github.com/hadrienl/aurelia-enhanced-template
Plus de détails en français sur mon blog : https://blog.hadrien.eu/2016/02/04/amelioration-progressive-avec-aurelia-io/
EDIT: <require> is not really working with this solution. I have to dig again :(
Change your MyCoolHeader's template from:
<template>
<content></content>
</template>
to:
<template>
<img src="logo.png">
<div show.bind="user">${user.name}</div>
<div show.bind="!user">Sign-in</div>
</template>
then change your server-generated page to something like this:
<html>
<body>
<my-cool-header></my-cool-header>
<div>Cachabled content</div>
</body>
</html>
Hope that helps. If this doesn't solve the problem or is not an acceptable solution, let me know.
Edit
After reading your reply and thinking about this a bit more I'm leaning towards removing the <my-cool-header> element. It's not providing any behavior, it only acts as a data loader, it's template is provided by the server-side rendering process and it's expected to be rendered outside of the aurelia templating system, there's no real need to re-render it. Here's what this approach would look like, let me know if it seems like a better fit:
<html>
<body>
<div class="my-cool-header">
<img src="logo.png">
<div show.bind="user">${user.name}</div>
<div show.bind="!user">Sign-in</div>
</div>
<div>Cachabled content</div>
</body>
</html>
import {MainData} from './main-data';
import {UserService} from './user';
export function configure(aurelia) {
const mainData = aurelia.container.get(MainData);
const userService = aurelia.container.get(UserService);
aurelia.use
.standardConfiguration()
.developmentLogging();
Promise.all([
this.userService.get(),
aurelia.start()
]).then(([user, a]) => {
mainData.user = user;
a.enhance(mainData, document.body);
});
}
To supplement Jeremy's answer, if you did change the template to:
<template>
<img src="logo.png">
<div show.bind="user">${user.name}</div>
<div show.bind="!user">Sign-in</div>
</template>
This content would be present when Aurelia processed the element and in the absence of a content selector, anything inside the custom element tags will be replaced by the template
If you then put your non-javascript content inside the custom element tags:
<my-cool-header>
<div>This stuff will be visible when JS is turned off</div>
</my-cool-header>
In the example above, in the absence of JS the div should still be there as Aurelia won't remove it from the DOM.
(This is of course assuming your server side tech doesn't mangle/fix the unknown HTML tags in the DOM for some reason when serving pages - which it probably won't since it would break Aurelia anyway)
EDIT:
The alternative you may be looking for is the #processContent decorator.
This allows you to pass a callback function that runs before Aurelia inspects the element.
At this point you could just lift the content between the custom element tags and add it as a child of the template element. The content should then be in scope of your viewmodel.
This way you can have the same markup in between the custom element tags with no javascript, and inside your template in the correct scope when Aurelia is running
import {processContent, TargetInstruction, inject} from 'aurelia-framework';
#inject(Element, TargetInstruction)
#processContent(function(viewCompiler, viewResources, element, instruction) {
// Do stuff
instruction.templateContent = element;
return true;
})
class MyViewModel {
constructor(element, targetInstruction) {
var behavior = targetInstruction.behaviorInstructions[0];
var userTemplate = behavior.templateContent;
element.addChild(userTemplate);
}
}
Disclaimer: the above code hasn't been tested and I pulled it from my grid which is several releases old - you may need to tweak

Categories

Resources