Stencil usage - how to load external dependent stencil component in stencil module - but not to add in final build - config

I am planning to create filter box web component using #stencil/core library.
In this filter component, i need to load external atomic web components like (dropdown, input, radio) implemented using #stencil/core itself. But i dont want these external web component to be part of final bundle of filter web component
Usage:
I want to use this filter component in host application using Angular.
And its responsibility of HOST application to import all atomic web component/external dependencies like dropdown/radio web component. Filter web component should not load these dependencies or take them in its build
Example:
**Dropdown **component's tsx file - bundled as dropdown npm package
import { Component, Prop, State, Event, EventEmitter, h } from '#stencil/core';
#Component({
tag: 'my-dropdown',
styleUrl: 'dropdown.scss'
})
export class myDropdown {
/**
* #public
* #property items
*
* Defines the data that we want to load into our dropdown area
*
*/
public items: Array<any> = [
{
heading: 'Virtual DOM',
description: 'A tree of custom objects representing a part of the DOM which can be acted upon quicker than manipulating the DOM itself'
},
{
heading: 'Async rendering',
description: 'Allows parts of a component state to be rendered asynchronously (I.e. via XHR)'
},
{
heading: 'Reactive data-binding',
description: 'Allows data binding to be implemented through binding a state variable to an onChange event which allows the state to be changed as the input value changes'
}
];
/**
* #public
* #property name
* #type String
*
* This will accept values supplied by a name attribute
* on the component HTML
*/
#Prop() name: string;
/**
* #type boolean
*
* This will track state changes (I.e. whether the
* dropdown component is open or closed)
*/
#State() toggle: boolean = false;
#Event({
eventName: 'itemClicked',
composed: true,
bubbles: true,
cancelable: true
}) itemClicked: EventEmitter;
/**
* #type EventEmitter
*
* Track component events (I.e. activation of dropdown component)
*/
#Event({
composed: true,
bubbles: true,
eventName: 'toggleDropdown',
cancelable: true
}) onToggle: EventEmitter;
/**
* #public
* #method toggleComponent
*
* This will manage the dropdown component event and state changes
*/
toggleComponent(): void {
this.toggle = !this.toggle;
// When the user click emit the toggle state value
// A event can emit any type of value
this.onToggle.emit({ visible: this.toggle });
}
onListItemClicked(ind) {
console.log(ind);
this.itemClicked.emit({ index: ind });
}
/**
* Create HTML representation of component DOM and return this
for output to the browser DOM
*/
render() {
return (
<div class="container">
<h2 onClick={() => this.toggleComponent()}>{this.name} {this.toggle ? <span>▲</span> : <span>▼</span>}</h2>
<ul class={this.toggle ? 'active' : 'inactive'}>
{this.items.map((item, index) => <li onClick={this.onListItemClicked.bind(this, index)}><h3>{item.heading}</h3><p>{item.description}</p></li>)}
</ul>
</div>
)
}
}
In my wrapper web component - MyComponent.tsx
import { Component, Prop, h, Listen } from '#stencil/core';
import { IButtonClickEvent } from '../simple-button/simple-button.model';
import 'dropdown';
#Component({
tag: 'my-component',
styleUrl: 'my-component.css',
shadow: true
})
export class MyComponent {
/**
* The first name
*/
#Prop() first: string;
/**
* The middle name
*/
#Prop() middle: string;
/**
* The last name
*/
#Prop() last: string;
private getText(): string {
return this.first + this.middle + this.last;
}
#Listen('buttonClicked')
onButtonClickHandler(evt:CustomEvent<IButtonClickEvent>){
console.log(evt)
}
#Listen('itemClicked')
itemClickedHandler(event: CustomEvent) {
console.log('Received the custom itemClicked event: ', event.detail);
}
render() {
return (
<div class="container" style={{
'font-size': '12px',
'padding':'10px',
'border':'2px solid red'
}}>
<div class="sc-abc abc">Hello, World! I'm {this.getText()}</div>
<simple-button type="raised" color="primary">Child</simple-button>
<my-dropdown name="Stencil key features"></my-dropdown>
</div>
);
}
}
I m loading this dropdown using 'import dropdown' - this works fine.
But issue with this is - stencil config considers this dropdown as well in final build of MyComponent. I want to ignore dropdown bundle to be included in final build.
Host application (Angular app) to install and load dropdown as per requirement

After some debugging found issue
Since i need to load dropdown component as external dependency, it cannot be imported in MyComponent tsx.
loaded external dropdown component in index.html directly via below script inclusion
<script type="module" src="/build/dropdown/dist/dropdown/dropdown.esm.js"></script>
<script nomodule src="/build/dropdown/dist/esm/dropdown.js"></script>
Also made changes in stencil config to copy dropdown component while working on local
import { Config } from '#stencil/core';
import { sass } from '#stencil/sass';
export const config: Config = {
namespace: 'demo',
outputTargets: [
{
type: 'dist',
esmLoaderPath: '../loader',
},
{
type: 'dist-custom-elements',
},
{
type: 'docs-readme',
},
{
type: 'www',
serviceWorker: null, // disable service workers
copy: [
{
src: '../node_modules/dropdown/',
dest: 'build/dropdown'
}
]
},
],
plugins: [sass()]
};

Related

How to leave existing class attribute on image element - now it is being moved to a generated enclosing span

Background: Trying to use ckeditor5 as a replacement for my homegrown editor in a non-invasive way - meaning without changing my edited content or its class definitions. Would like to have WYSIWYG in the editor. Using django_ckeditor_5 as a base with my own ckeditor5 build that includes ckedito5-inspector and my extraPlugins and custom CSS. This works nicely.
Problem: When I load the following HTML into ClassicEditor (edited textarea.value):
<p>Text with inline image: <img class="someclass" src="/media/uploads/some.jpeg"></p>
in the editor view area, browser-inspection of the DOM shows:
...
<p>Text with an inline image:
<span class="image-inline ck-widget someclass ck-widget_with-resizer" contenteditable="false">
<img src="/media/uploads/some.jpeg">
<div class="ck ck-reset_all ck-widget__resizer ck-hidden">
<div ...></div></span></p>
...
Because the "someclass" class has been removed from and moved to the enclosing class attributes, my stylesheets are not able to size this image element as they would appear before editing.
If, within the ckeditor5 view, I edit the element using the browser inspector 'by hand' and add back class="someclass" to the image, ckeditor5 displays my page as I'd expect it with "someclass" and with the editing frame/tools also there. Switching to source-editing and back shows the class="someclass" on the and keeps it there after switching back to document editing mode.
(To get all this, I enabled the GeneralHtmlSupport plugin in the editor config with all allowed per instructions, and that seems to work fine.) I also added the following simple plugin:
export default class Extend extends Plugin {
static get pluginName() {
return 'Extend';
}
#updateSchema() {
const schema = this.editor.model.schema;
schema.extend('imageInline', {
allowAttributes: ['class']
});
}
init() {
const editor = this.editor;
this.#updateSchema();
}
}
to extend the imageInline model hoping that would make the Image plugin keep this class attribute.
This is the part where I need some direction on how to proceed - what should be added/modified in the Image Plugin or in my Extend plugin to keep the class attribute with the element while editing - basically to fulfill the WYSIWYG desire?
The following version does not rely on GeneralHtmlSupport but creates an imageClassAttribute model element and uses that to convert only the image class attribute and place it on the imageInline model view widget element.
import Plugin from '#ckeditor/ckeditor5-core/src/plugin';
export default class Extend extends Plugin {
static get pluginName() {
return 'Extend';
}
#updateSchema() {
const schema = this.editor.model.schema;
schema.register( 'imageClassAttribute', {
isBlock: false,
isInline: false,
isObject: true,
isSelectable: false,
isContent: true,
allowWhere: 'imageInline',
});
schema.extend('imageInline', {
allowAttributes: ['imageClassAttribute' ]
});
}
init() {
const editor = this.editor;
this.#updateSchema();
this.#setupConversion();
}
#setupConversion() {
const editor = this.editor;
const t = editor.t;
const conversion = editor.conversion;
conversion.for( 'upcast' )
.attributeToAttribute({
view: 'class',
model: 'imageClassAttribute'
});
conversion.for( 'dataDowncast' )
.attributeToAttribute({
model: 'imageClassAttribute',
view: 'class'
});
conversion.for ( 'editingDowncast' ).add( // Custom conversion helper
dispatcher =>
dispatcher.on( 'attribute:imageClassAttribute:imageInline', (evt, data, { writer, consumable, mapper }) => {
if ( !consumable.consume(data.item, evt.name) ) {
return;
}
const imageContainer = mapper.toViewElement(data.item);
const imageElement = imageContainer.getChild(0);
if ( data.attributeNewValue !== null ) {
writer.setAttribute('class', data.attributeNewValue, imageElement);
} else {
writer.removeAttribute('class', imageElement);
}
})
);
}
}
Well, Mr. Nose Tothegrind found two solutions after digging through ckeditor5 code, here's the first one. This extension Plugin restores all image attributes that are collected by GeneralHtmlSupport. It can be imported and added to a custom ckeditor5 build app.js file by adding config.extraPlugins = [ Extend ]; before the editor.create(...) statement.
import Plugin from '#ckeditor/ckeditor5-core/src/plugin';
import GeneralHtmlSupport from '#ckeditor/ckeditor5-html-support/src/generalhtmlsupport';
export default class Extend extends Plugin {
static get pluginName() {
return 'Extend';
}
static get requires() {
return [ GeneralHtmlSupport ];
}
init() {
const editor = this.editor;
this.#setupConversion();
}
#setupConversion() {
const editor = this.editor;
const t = editor.t;
const conversion = editor.conversion;
conversion.for ( 'editingDowncast' ).add( // Custom conversion helper
dispatcher =>
dispatcher.on( 'attribute:htmlAttributes:imageInline', (evt, data, { writer, mapper }) => {
const imageContainer = mapper.toViewElement(data.item);
const imageElement = imageContainer.getChild(0);
if ( data.attributeNewValue !== null ) {
const newValue = data.attributeNewValue;
if ( newValue.classes ) {
writer.setAttribute('class', newValue.classes.join(' '), imageElement);
}
if ( newValue.attributes ) {
for (const name of Object.keys(newValue.attributes)) {
writer.setAttribute( name, newValue.attributes[name], imageElement);
}
}
} else {
writer.removeAttribute('class', imageElement);
}
})
);
}a
}

Do Vue.js render functions allow return of an array of VNodes?

I am working on extending a Vue.js frontend application. I am currently inspecting a render function within a functional component. After looking over the docs, I had the current understanding that the render function within the functional component will return a single VNode created with CreateElement aka h.
My confusion came when I saw a VNode being returned as an element in an array. I could not find any reference to this syntax in the docs. Does anyone have any insight?
export default {
name: 'OfferModule',
functional: true,
props: {
data: Object,
placementInt: Number,
len: Number
},
render (h, ctx) {
let adunitTheme = []
const isDev = str => (process.env.dev ? str : '')
const i = parseInt(ctx.props.placementInt)
const isDevice = ctx.props.data.Component === 'Device'
const Component = isDevice ? Device : Adunit
/* device helper classes */
const adunitWrapper = ctx.props.data.Decorate?.CodeName === 'AdunitWrapper'
if (!isDevice /* is Adunit */) {
const offerTypeInt = ctx.props.data.OfferType
adunitTheme = [
'adunit-themes',
`adunit-themes--${type}`.toLowerCase(),
`adunit-themes--${theme}`.toLowerCase(),
`adunit-themes--${type}-${theme}`.toLowerCase(),
]
}
const renderOfferModuleWithoutDisplayAdContainersWithAboveTemplate =
ctx.props.data.Decorate?.Position === 'AboveAdunit' || false
const renderOfferModuleWithoutDisplayAdContainers =
ctx.props.data.Decorate?.RemoveAds /* for adunits */ ||
ctx.props.data.DeviceData?.RemoveAds /* for devices */ ||
false
const getStyle = (className) => {
try {
return ctx.parent.$style[className]
} catch (error) {
console.log('$test', 'invalid style not found on parent selector')
}
}
const PrimaryOfferModule = (aboveAdunitSlot = {}) =>
h(Component, {
props: {
data: ctx.props.data,
itemIndex: i,
adunitTheme: adunitTheme.join('.')
},
attrs: {
class: [
...adunitTheme,
getStyle('product')
]
.join(' ')
.trim()
},
scopedSlots: {
...aboveAdunitSlot
}
})
if (renderOfferModuleWithoutDisplayAdContainersWithAboveTemplate) {
return [
PrimaryOfferModule({
aboveAdunit (props) {
return h({
data () {
return ctx.props.data.Decorate
},
template: ctx.props.data.Decorate?.Template.replace(
'v-show="false"',
''
)
})
}
})
]
} else if (renderOfferModuleWithoutDisplayAdContainers) {
return [PrimaryOfferModule()]
} else {
const withAd = i > 0 && i % 1 === 0
const adWrap = (placement, position, className) => {
return h(
'div',
{
class: 'm4d-wrap-sticky'
},
[
h(Advertisement, {
props: {
placement,
position: String(position)
},
class: getStyle(className)
})
]
)
}
return [
withAd && adWrap('inline-sticky', i, 'inlineAd'),
h('div', {
class: 'm4d-wrap-sticky-adjacent'
}),
h(
'div',
{
attrs: {
id: `inline-device--${String(i)}`
},
class: 'inline-device'
},
isDev(`inline-device id#: inline-device--${String(i)}`)
),
withAd &&
i !== ctx.props.len - 1 &&
h(EcomAdvertisement, {
props: {
placement: 'inline-static',
position: String(i)
},
class: getStyle('inlineStaticAd')
}),
PrimaryOfferModule()
]
}
}
}
It turns out that returning an array of VNodes actually predates the scopedSlots update.
I couldn't find it documented anywhere in the docs either, but via this comment on a Vue GitHub issue by a member of the Vue.js core team (which predates the scopedSlots commit by ~1 year), render() can return an Array of VNodes, which Vue will take and render in order. However, this only works in one, singular case: functional components.
Trying to return an array of VNodes with greater than 1 element in a normal (non-functional, stateful) component results in an error:
Vue.config.productionTip = false;
Vue.config.devtools = false;
Vue.component('render-func-test', {
render(h, ctx) {
return [
h('h1', "I'm a heading"),
h('h2', "I'm a lesser heading"),
h('h3', "I'm an even lesser heading")
];
},
});
new Vue({
el: '#app',
});
<script src="https://unpkg.com/vue#2/dist/vue.js"></script>
<div id="app">
Test
<render-func-test></render-func-test>
</div>
[Vue warn]: Multiple root nodes returned from render function. Render function should return a single root node.
But doing this in a functional component, as your example does, works just fine:
Vue.config.productionTip = false;
Vue.config.devtools = false;
Vue.component('render-func-test', {
functional: true, // <--- This is the key
render(h, ctx) {
return [
h('h1', "I'm a heading"),
h('h2', "I'm a lesser heading"),
h('h3', "I'm an even lesser heading")
];
},
});
new Vue({
el: '#app',
});
<script src="https://unpkg.com/vue#2/dist/vue.js"></script>
<div id="app">
Test
<render-func-test></render-func-test>
</div>
If you're interested in the why, another member of the Vue core team explained this limitation further down in the thread.
It basically boils down to assumptions made by the Vue patching and diffing algorithm, with the main one being that "each child component is represented in its parent virtual DOM by a single VNode", which is untrue if multiple root nodes are allowed.
The increase in complexity to allow this would require large changes to that algorithm which is at the very core of Vue. This is a big deal, since this algorithm must not only be good at what it does, but also very, very performant.
Functional components don't need to conform to this restriction, because "they are not represented with a VNode in the parent, since they don't have an instance and don't manage their own virtual DOM"– they're stateless, which makes the restriction unnecessary.
It should be noted, however, that this is possible on non-functional components in Vue 3, as the algorithm in question was reworked to allow it.
It seems this was implemented in:
https://github.com/vuejs/vue/commit/c7c13c2a156269d29fd9c9f8f6a3e53a2f2cac3d
This was a result of an issue raised in 2018 (https://github.com/vuejs/vue/issues/8056) , because this.$scopedSlots.default() returned both a VNode or an array of VNodes depending on the content.
The main argument was that this is inconsistent with how regular slots behave in render functions, and means any render function component rendering scoped slots as children needs to type check the result of invoking the slot to decide if it needs to be wrapped in an array
So Evan comments on the issue thread here, explaining that this.$scopedSlots.default would always return Arrays beginning v2.6 to allow for consistency, but to avoid breaking changes for how $scopedSlots was being used, the update would also allow return of an Array of a single VNode from render functions as well.

Full Calendar Vue JS Component - Adding Events

I'm using Full Calendar with Vue JS: https://fullcalendar.io/docs/vue
I can't seem to find a way to add events using the Full Calendar Vue JS component. The only way I see the examples doing it is by getting ahold of the calendar's API and creating events through it. This seems a little anti-vue.
Demo from docs:
handleDateSelect(selectInfo) {
let title = prompt('Please enter a new title for your event')
let calendarApi = selectInfo.view.calendar
calendarApi.unselect() // clear date selection
if (title) {
calendarApi.addEvent({
id: createEventId(),
title,
start: selectInfo.startStr,
end: selectInfo.endStr,
allDay: selectInfo.allDay
})
}
}
I'm wondering, is the only way to create an event on the Vue JS Full Calendar by tapping into the calendars native API as shown above? Are there no means of sending an event of sorts into the component?
You don't really have to fallback to using imperative instance API. The Vue FullCalendar components exposes the events as part of the options which you can use. For example:
<template>
<FullCalendar :options="opts" />
<button #click="addNewEvent"></button>
</template>
In the component definition, you can use the events key to set list of events declaratively. Every time, you need to add/remove the events just modify the events key which is part of your options object.
export default {
data() {
return {
opts: {
plugins: [ /* Any addition plugins you need */ ],
initialView: 'dayGridMonth',
events: [
{ title: 'First Event', date: '2021-05-12' },
/* Few more initial events */
]
}
}
},
methods: {
addNewEvent() {
this.opts.events = [
...this.opts.events,
{ title: 'Another Event', date: '2021-05-13' }
];
}
}
}

How does an aframe entity defaults to the schema ones if no data is passed on

I'm having some difficulties in understanding how in aFrame an entity works with schema and data together. Forgive me, I'm a bit new in the library and maybe I'm missing something. Thanks.
Consider the following:
// register the aframe component (Button.js):
import { registerComponent, THREE } from 'aframe'
const button = registerComponent('button', {
schema: {
width: { type: 'number', default: 0.6 },
height: { type: 'number', default: 0.40 },
color: { type: 'string', default: 'orange' },
},
init: function() {
const schema = this.schema // Schema property values.
const data = this.data // Component property values.
const el = this.el // Reference to the component's entity.
// Create geometry.
this.geometry = new THREE.BoxBufferGeometry(data.width || schema.width.default, data.height || schema.height.default, data.depth || schema.depth.default)
// Create material.
this.material = new THREE.MeshStandardMaterial({ color: data.color || schema.color.default })
// Create mesh.
this.mesh = new THREE.Mesh(this.geometry, this.material)
// Set mesh on entity.
el.setObject3D('mesh', this.mesh)
}
})
export default button
// add the entity in the app (App.js)
import button from './Button'
<a-entity button="width: 0.4; height: 0.4: color: 'green'" position="0 0 0"></a-entity>
Now with the example above I would expect that my component will use data instead of schema defaults. And if I would instead just do:
<a-entity button position="0 0 0"></a-entity>
It would use the schema default ones.
Is this how it should be done?
I've been seeing a lot of examples where people just use data.
Thanks
The schema isn't intended to be used directly. From the docs:
The schema defines the properties of its component. (...)
The component’s property type values are available through this.data
The idea goes like this:
You define the properties (with defaults) in the schema
schema {
prop: {default: "default value"}
},
You access the values with this.data[prop_name]:
init: function() {
console.log("prop value at init:", this.data.prop)
},
You can monitor the property updates (like via setAttribute("propname", "newValue") within the update handler:
update: function(oldData) {
console.log("prop changed from", oldData.prop, "to", this.data.prop)
},
In a setup like this:
<!-- this.data.prop will contain the default values -->
<a-entity foo></a-entity>
<!-- this.data.prop will contain the new value -->
<a-entity foo="prop: value></a-entity>

Mat-select is not showing the options: Angular 10

I wonder what is wrong with my use of mat-select. Because it is not showing the list of options:
<form>
<mat-form-field appearance="standard">
<mat-label>New Error Type</mat-label>
<mat-select [(ngModel)]="selectedNewErrorCode" name="faultType">
<mat-option *ngFor="let faultType of faultTypes" [value]="faultType.code">
{{faultType.label}}
</mat-option>
</mat-select>
</mat-form-field>
<p>Selected error: {{selectedNewErrorCode}}</p>
</form>
The component which is displayed in a modal is the following.
/** Imports animations */
import {slideInAnimation} from '../../animations';
/** Imports models */
import {StackTraceView} from '../../objects/stack-trace-view.model';
import {FaultType} from '../../objects/fault-type.model';
#Component({
selector: 'app-consult-details',
templateUrl: './consult-details.component.html',
styleUrls: ['./consult-details.component.sass'],
animations: [slideInAnimation]
})
export class ConsultDetailsComponent implements OnInit {
constructor() {
}
public flux: StackTraceView;
public modifState = false;
/** Used in resetting the form */
originalFlux: string;
faultTypes: FaultType[];
/** determines if the original error flux should be visible or not */
public originalErrorVisible = false;
/** Sets the new fault type for reanalysing the stack trace */
selectedNewErrorCode: string;
ngOnInit(): void {
}
modifyFlux() {
this.modifState = !this.modifState;
}
sendFlux() {
console.log(`The flux changed to ${this.flux.originalFlux}`);
}
/** Reste the form to its original state */
resetForm() {
document.getElementById('toBeReset').innerHTML = this.originalFlux;
this.flux.originalFlux = this.originalFlux;
this.modifState = false;
}
/** Sets the visibility of the original error flux to flse if it is true and vice versa */
isOriginalErrorVisible() {
this.originalErrorVisible = !this.originalErrorVisible;
}
}
The entire component is displayed in a modal. The variable faultTypes is fed in when the modal is called in the parent component. The corresponding code in the parent component is the following:
const detailsContent = this.specificReportList.filter(
entry => entry.messageId === originalMessageId
)[0];
const modalRef = this.modalService.open(ConsultDetailsComponent, {
size: 'xl',
ariaDescribedBy: 'Details'
});
/** The input sata for the pop-up component */
modalRef.componentInstance.flux = detailsContent;
modalRef.componentInstance.originalFlux = detailsContent.originalFlux;
modalRef.componentInstance.faultTypes = this.faultTypeList;
modalRef.result.then((result) => {
this.closeResult = `Close with ${result}`;
}, (reason) => {
this.closeResult = `Dismissed ${this.getDismissReason(reason)}`;
});
}```
As a final comment the variable faulttypes is well fed in via the
parent component and when I use nromal select and option I do not have
any problem; The code works very well, the only problem is when I use
mat-select which is important for me beacuse It gives a unifrom look
and feel to my application.