Pass global styles down to Svelte "custom element" components - javascript

I am "attempting" to use Sveltejs as a front-end web framework for a project built on Django (relevant as it defines the structure of my application). I am attempting to include Svelte components onto the various templates I have built for my Django application. Now, using the customElement API, I was able to compile and use my Svelte components as custom elements directly in my HTML templates which was great except for one major problem: my global styles weren't propagating to the components. This is, as I found out, because all custom elements are actually compiled as web components, meaning they wrap their internal HTML to prevent styles from affecting them. This is not the behavior I want. However, it seems to be the only real way to use my Svelte components in a way that works cohesively with my HTML templates. How do I solve this problem?
I am using webpack as my module bundler. I can comment a gist link to my webpack.config.js if it would be helpful, but as you can imagine, I am compiling for multiple Django "apps" and so my config is a bit messy. For reference, I have gotten everything including local styling with Sass and custom elements to work as intended. This is simply a question of "how to do something" which despite me spending literal hours Googling, I have been unable to find a clear answer to.
I know you can use the client API directly to access components, but not only is this tedious and messy (especially if I need to compose components on different HTML templates together), but I can't seem to get webpack to expose my Svelte components as Javascript classes. Here is how I am doing this approach (which is not working):
<!-- in the head -->
<script src="bundle.js"></script>
<!-- in the body -->
<script>new bundle.App{target: ...}</script>
It is telling me that App is not defined. Here is what my individual output configs in my webpack.config.js look like:
output: {
library: 'bundle',
// I do not understand at all what these two lines do -- I just found
// them somewhere on the interwebs as a suggestion to solve this problem
libraryTarget: 'umd',
umdNamedDefine: true,
// path, filename, etc.
}
To boil this down, I really have three, intertwined questions:
Can I use the customElement API (which I much prefer) and still apply global styles?
If I can't use the customElement API, is there a better approach to this problem that would allow for global styles?
If there is no other option, how do I properly use the client API with webpack?

TL;DR: There is no clean/perfect answer for this.
For once, there is no way to inject global styles into the Shadow Dom. Having said that there are few things you can try.
First, if you are not using slots, then you can write your own custom element registration function and use that to register elements. You will have to write your own adapter for web components that extends from HTMLElement class. In this approach, each Svelte component would be independent app that you are simply initializing from your web component. This is the best alternative you can explore
Additionally, you can use Constructable Stylesheets. It allows you to programmatically construct a stylesheet object and attach it to a Shadow DOM. Of course, this work only when you have flat components. When your web components are nested within one-another, each would have its Shadow DOM. You would have to create a common global style as a constructable stylesheet and attach to each component. Look here for the example:
const sheet = new CSSStyleSheet();
// Replace all styles synchronously for this style sheet
sheet.replaceSync('p { color: green; }');
class FancyComponent1 extends HTMLElement {
constructor() {
super();
const shadowRoot = this.attachShadow({ mode: 'open' });
// Attaching the style sheet to the Shadow DOM of this component
shadowRoot.adoptedStyleSheets = [sheet];
shadowRoot.innerHTML = `
<div>
<p>Hello World</p>
</div>
`;
}
}
class FancyComponent2 extends HTMLElement {
constructor() {
super();
const shadowRoot = this.attachShadow({ mode: 'open' });
// Same style sheet can also be used by another web component
shadowRoot.adoptedStyleSheets = [sheet];
// You can even manipulate the style sheet with plain JS manipulations
setTimeout(() => shadowRoot.adoptedStyleSheets = [], 2000);
shadowRoot.innerHTML = `
<div>
<p>Hello World</p>
</div>
`;
}
}
In above example, the sheet is a common stylesheet which is being used in two separate web components. But again, you will have to write you own web component wrapper to achieve this.

Is there any performance (or otherwise ) downside to using the #import directive inside custom elements?
This is how I pass global styles to svelte custom elements:
/assets/theme.css
---
:root,:host{
--some-var:1rem;
}
Then inside the component:
CustomElement.svelte
---
<svelte:options tag="custom-element"/>
...
<style>
#import "/assets/theme.css";
:host{
padding:var(--some-var)
}
</style>

Related

How to specify template for Glimmer Component?

I have a typical Glimmer "base" component:
import Component from '#glimmer/component';
export default class BaseComponent extends Component { ... }
It has a template like normally, but the actual implementations of that component are child componenents, that override some of the template getters and parameters so that it works with various different data types.
export default class TypeAComponent extends BaseComponent { ... }
export default class TypeBComponent extends BaseComponent { ... }
etc.
My question is: How do I specify that all the child components should use the parent class template, so I don't have to duplicate the same fairly complex HTML for all child components? Visually the components are supposed to look identical so any changes would have to be replicated across all child component types. Therefore multiple duplicated templates isn't ideal.
In Ember Classic components there was layout and layoutName properties so I could just do:
layoutName: 'components/component-name'
in the base component and all child components did automatically use the defined template.
Now that I'm migrating to Glimmer components I can't seem to figure out how to do this. I have tried:
layout property
layoutName property
template property
Using the child components without a template in hope that they would automatically fall back to the parent class template.
Only thing that seems to work is creating Application Initializer like this:
app.register('template:components/child1-component', app.lookup('template:components/base-component'));
app.register('template:components/child2-component', app.lookup('template:components/base-component'));
But that feels so hacky that I decided to ask here first if there is a proper way to do this that I have missed?
How to specify template for Glimmer Component?
tl;dr: you should avoid this.
There are two answers to two, more specific, questions:
What is the recommended way to manage complex components with shared behaviors?
Typically, you'll want to re-work your code to use either composition or a service.
Composition
<BaseBehaviors as |myAPI|>
<TypeAComponent #foo={{myAPI.foo}} #bar={{myAPI.bar}} />
<BaseBehaviors>
Where BaseBehaviors' template is:
{{yield (hash
foo=whateverThisDoes
bar=whateverThisBarDoes
)}}
Service
export default class TypeAComponent extends Component {
#service base;
}
and the service can be created with
ember g service base
then, instead of accessing everything on this, you'd access everything on this.base
Ignoring all advice, how do I technically do the thing?
Co-located components (js + hbs as separate files), are combined into one file at build time, which works like this:
// app/components/my-component.js
import Component from '#glimmer/component';
export default class MyComponent extends Cmoponent {
// ..
}
{{! app/components/my-component.hbs }}
<div>{{yield}}</div>
The above js and hbs file becomes the following single file:
// app/components/my-component.js
import Component from '#glimmer/component';
import { hbs } from 'ember-cli-htmlbars';
import { setComponentTemplate } from '#ember/component';
export default class MyComponent extends Cmoponent {
// ..
}
setComponentTemplate(hbs`{{! app/components/my-component.hbs }}
<div>{{yield}}</div>
`, MyComponent);
So this means you can use setComponentTemplate anywhere at the module level, to assign a template to a backing class.
Why is this not recommended over the other approaches?
All of this is a main reason the layout and related properties did not make it in to Octane.
Formally supported Component inheritance results in people getting "clever"
this in of itself, isn't so much of a problem, as it is what people can do with the tool. Bad inheritance is the main reason folks don't like classes at all -- and why functional programming has been on the rise -- which is warranted! Definitely a bit of an over-correction, as the best code uses both FP and OP, when appropriate, and doesn't get dogmatic about this stuff.
Component Inheritance is harder to debug
Things that are a "Foo" but are a subclass of "Foo" may not actually work like "Foo", because in JS, there aren't strict rules around inheritance, so you can override getters, methods, etc, and have them provide entirely different behavior.
This confuses someone who is looking to debug your code.
Additionally, as someone is trying to do that debugging, they'll need to have more files open to try to under stand the bigger picture, which increases cognitive load.
Component inheritance allows folks to ignore boundaries
This makes unit testing harder -- components are only tested as "black boxes" / something you can't see in to -- you test the inputs and outputs, and nothing in between.
If you do want to test the in-between, you need to extract either regular functions or a service (or more rendering tests on the specific things).
I would say this is the classic case for a composition, where TypeAComponent and TypeBComponent use the BaseComponent.
So you have your BaseComponent with all the HTML, that basically is your template. I think its important here to think a bit more of Components also as possible Templates, not only full Components. So lets call this the TemplateComponent.
So you have your TemplateComponent which could also be a template-only component. Then you have as template for TypeAComponent and TypeBComponent:
<TemplateComponent
#type={{#title}}
#title={{#title}}
#onchange={{#onchange}}
#propertyThatIsChanged={{this.propertyThatIsChanged}}
...
/>
this allows you to have a getter propertyThatIsChanged to overwrite pieces. Common behaviour can also be placed on the TemplateComponent, or, if its common code, maybe on a BaseCodeComponent, that only contains shared code, while I would rather not do this.
For areas you want to replace this also opens the possibility to use Blocks.
The TemplateComponent, for example, could use has-block to check if a :title exists, use this block then ({{yield to="default"}}), and if not just use {{#title}}.
So, to the only obvious downside of this: you have to proxy all params. This seems ugly at first, but generally I think its better for components not to have too many arguments. At some point an options or data argument could be better, also because it can be built with js when necessary. It should also be mentioned that there is an open RFC that would address this issue. With the upcoming SFCs, I think this is the much more future-proof solution overall.

How to create vanilla javascript widgets as web components and import into a page dynamically?

I am working on a web application which can host mini-apps (or modules) developed in vanilla Js, HTML, CSS.
The host application dynamically loads (using fetch API) the mini-apps (or modules) into its pages and then I want these mini-apps to independently request for their data or do whatever they want to. I want these mini-apps isolated from the host scripts and styling but the host should be able to execute functions of these mini-apps (or modules).
Example: The dashboard of Microsoft Azure portal. It has widgets which can be selected, customised and placed by the user, and after loading of the host dashboard these widgets independently fetch for their data. Also, the period and auto-refresh time can be controlled by the host application.
Priorities:
• Modules should be able to execute their own JS scripts.
• If possible then everything should be in vanilla js (or Stenciljs / Vue.js)
Current File Structure:
main.html
js (dir)
style (dir)
modules (dir)
• module-one
| module.html
| module.css
| module.js
• module-n
...
I have tried creating custom HTML element and then appending HTML and CSS of module to shadowDom. But I still don't know how to get its JS working. If I insert module.js dynamically to main.html then somehow I need to change roots of all module.js appended in host application from
document to shadowRoot
Example:
//module.js
const sayHelloBtn = document.getElementById('sayHello');
sayHelloBtn.addEventListener('click', () => {console.log('Hello')});
//module.js after appending to host (main.html)
const mod = document.querySelector('module-one').shadowRoot;
const sayHelloBtn = mod.getElementById('sayHello');
sayHelloBtn.addEventListener('click', () => {console.log('Hello')});
Please let me know if further elaboration or clarification on question is required. Thank You :)
Problem Update:
I get a json of all the modules from an API and dynamically create custom elements with shadow root . I fetch the module files using the fetch API and then append them to the custom elements. I am able to successfully append the .html and .css to respective custom elements but cannot run JS inside shadow DOM. If I dynamically import the JS globally to the main.html then some how I need the JS to access the elements in its shadowRoot and also the functions should not conflict with other module's js functions with same name.
I have tried creating classes in each module.js which holds its respective methods, variable and a init() which does all the module's initialisation.
//module.js
class ModuleAbc {
constructor(host = document) { // shadowRoot is passed when instantiating from the host application
this.docRoot = host;
console.log('docRoot set: ', this.docRoot);
}
init() {
console.log('initialising module at host: ', this.docRoot);
const docRoot = this.docRoot;
const btn = docRoot.getElementById('cm'); //this element is inside shadowRoot
btn.addEventListener('click', () => {
console.log('Hello from mod 1!');
});
}
}
Now I do not know how to call init() of all the classes because their names are different for every module.
//HOST JS (main.js)
let mod_name = 'ModuleAbc';
const mod = new mod_name(module_root); //THIS DOESN'T WORK
mod.init();
Problem is resolved using Estus Flask's suggested solution

Vanilla WebComponents approaches: is there any real difference between "importing js from html" and "fetching html from js" file

Context: until now I didn't mind about how import template html file to my vanilla webcomponent because I always wrote small html codes. So I have coded html on top of my webcomponent .js file and do something like:
const template = document.createElement("template");
template.innerHTML = `<div id="firstdiv"><input id="inputAny"/></div>`;
class anyVanillaWebComponent extends HTMLElement {
...
connectedCallback() {
this.attachShadow({ mode: "open" });
this.shadowRoot.appendChild(template.content.cloneNode(true));
const inputAny = this.shadowRoot.getElementById("inputAny");
...
This is quite common find in tutorials, blogs and foruns. Now I want to separate html from javascript assuming this will make my project tree more clear.
Searching around I found some discussion based on assumption that "import" will not be supported anymore in Browsers [(see UPDATE discussion on bottom about alternatives to import)].1
Basically this drove me to two possibilities:
1 - importing .js file to html from html
Exemplifying:
<template id="my-webcomponent-template">
<link rel="stylesheet" href="./some.css">
<div>some content ...</div>
</template>
<script src="MyWebcomponent.js"></script>
2 - fetch asynchronously my-webcomponent.html from .js file
(async () => {
const res = await fetch('/MyWebcomponent.html');
const textTemplate = await res.text();
const HTMLTemplate = new DOMParser().parseFromString(textTemplate, 'text/html')
.querySelector('template');
class MyWebcomponent extends HTMLElement {...
Based on such discussion from 2017, it seems I should avoid the option 1 but it is not clear for me why and if there is some real advantage on option 2. So my straight question is: is there any real diference between "importing" and "fetching" html file while coding Vanilla Webcomponents which is expected to be rendered straight by browsers that support natively Webcomponents (for instance, Chrome)?
If you don't plan to use your custom element in various pages then the 2 solutions are good.
In the first one it could be faster because you will save an HTTP request, for the HTML template is immediately available in the main HTML page.
But if you plan to reuse the custom element (or for better code separation), the second solution is better because the consumer is separated from the web component.
You can always look at a bundler/packager. Webpack and others work well.
I wrote this one specifically for Web Components:
https://www.npmjs.com/package/component-build-tools
It allows zero or more templates as real HTML files and allows importation of Locale based strings if you want to localize your components.
It also allows you to write your component using ES6 import and then cross-compile to ES5 if needed and allows output as an ES6 module file, a CJS file or a simple IIFE file.

How can I import compiled svelte-components/-apps into a svelte-app at runtime

Is it possible to dynamically import precompiled svelte components or whole svelte apps.
And when, how do I compile a single component in svelte 3. I found this approach, but nothing in the docs:
https://github.com/sveltejs/svelte/issues/1576
I want to combine several independent (hosted) Svelte apps on one page to one bigger svelte-app (microfrontend). The goal is, that every sub app can be independent deployed and hosted wherever (may be an own docker container). And any change should be visible in the aggregator app without recompiling it.
I think I wat to do something like this:
https://single-spa.js.org/docs/separating-applications.html
but with no other framework, that is blowing my app and components.
I don't want to use custom components, because of the inflexible styling of the Shadow DOM. I must be able to change css over a global stylesheet.
Has anyone an idea?
Thank you :)
You can take a look at Ara Framework. It has a standalone component named Nova Bridge.
This approach consists of a component that renders a placeholder micro-frontend view will be mounted. Then the component emits a DOM event named NovaMount that is listened by the JavaScript bundle of the micro-frontend to render it and mount it in run-time.
Here an example of the entry point for your micro-frontend.
import { mountComponent, load, loadById } from 'hypernova-svelte'
import Example from './components/Example.svelte'
const render = (name, { node, data }) => {
if (name === 'Example') {
return mountComponent(Example, node, data)
}
}
document.addEventListener('NovaMount', ({ detail }) => {
const { name, id } = detail
const payload = loadById(name, id)
if (payload) {
render(name, payload)
}
})
load('Example').forEach(render.bind(null, 'Example'))
The microfrontend uses hypernova-svelte. You can take a look in this article I wrote for implementing Svelte in Nuxt.
https://ara-framework.github.io/website/blog/2019/08/27/nuxt-js

How do I register a Vue component?

I have the following files. All I want to do is to be able to create different components that are injected. How do I achieve this using require.js? Here are my files:
main.js
define(function(require) {
'use strict';
var Vue = require('vue');
var myTemplate = require('text!myTemplate.html');
return new Vue({
template: myTemplate,
});
});
myTemplate.html
<div>
<my-first-component></my-first-component>
</div>
MyFirstComponent.vue
<template>
<div>This is my component!</div>
</template>
<script>
export default {}
</script>
I'm going to assume you're using webpack as explained in the Vue.js docs, or else your .vue file is useless. If you're not, go check how to set up a webpack Vue app first, it's what lets you use .vue files.
import Menubar from '../components/menubar/main.vue';
Vue.component('menubar', Menubar);
That's how you add e.g. a menubar component to the global scope. If you want to add the component to just a small part of your app, here's another way of doing it (this is taken from inside another component, but can be used in exactly the same manner on your primary Vue object):
import Sidebar from '../../components/sidebar/main.vue';
export default {
props: [""],
components: {
'sidebar': Sidebar
},
...
You can load components without webpack, but I don't recommend it, if you're gonna keep using Vue (which I strongly suggest you do) it's worth it to look into using webpack.
Update
Once again, really, really, really consider using webpack instead if you're gonna be continuing with Vue.js, the setup may be slightly more annoying but the end result and development process is waaaay better.
Anyway, here's how you'd create a component without webpack, note that without webpack you can't use .vue files since the .vue format is part of their webpack plugin. If you don't like the below solution you can also use e.g. ajax requests to load .vue files, I believe there is a project somewhere out there that does this but I can't find it right now, but the end result is better with webpack than with ajax anyway so I'd still recommend going with that method.
var mytemplate = `<div>
<h1>This is my template</h1>
</div>`
Vue.component('mycomp1', {
template: mytemplate
});
Vue.component('mycomp2', {
template: `
<div>
Hello, {{ name }}!
</div>
`,
props: ['name'],
});
As you can see, this method is A LOT more cumbersome. If you want to go with this method I'd recommend splitting all components into their own script files and loading all those components separately prior to running your actual app.
Note that `Text` is a multi line string in javascript, it makes it a little easier to write your template.
And as I said, there is some project out there for loading .vue files using ajax, but I can't for the life of me find it right now.

Categories