How can I access the host of a custom element - custom-controls

I have a custom element which itself hosts a custom element.
<polymer-element name="flex-nonvisual">
<template>
<polymer-flex-layout></polymer-flex-layout>
</template>
</polymer-element>
Now in attached() (or some other callback) of PolymerFlexLayout I want to set the class attribute of the flex-nonvisual element.
In Javascript the code looks like this.parentNode.host.classList.add('someclass');
In Dart in attached() (after the call to super.attached()) this.parent is null
and I couldn't find any other reference to the host element.
How can I do this in Dart?

Unfortunately creation order of custom elements is not guaranteed. See the Polymer.dart discussion and the Related discussion on the Polymer discussion groups.
However as mentioned, your particular usage will break encapsulation and using CustomEvents is much more the way to go. And using Polymer this becomes very easy to implement as well.
<!-- flex_nonvisual.html file -->
<polymer-element name="flex-nonvisual">
<template>
<polymer-flex-layout on-ready="{{layoutReady}}"></polymer-flex-layout>
</template>
</polymer-element>
// polymer_flex_layout.dart file
#CustomTag('polymer-flex-layout')
class PolymerFlexLayout extends PolymerElement {
// other stuff here
void attached() {
super.attached();
dispatchEvent(new CustomEvent('ready'));
}
}
// flex_nonvisual.dart
#CustomTag('flex-nonvisual')
class FlexNonvisual extends PolymerElement {
// Other stuff here
void layoutReady(Event e, var details, Node node) {
this.classes.add('someclass');
}
}

update: Polymer >=1.0.x
shady DOM
new PolymerDom(this).parentNode;
or
domHost
short for
Polymer.dom(this).getOwnerRoot().host
full shadow DOM
(this.parentNode as ShadowRoot).host
#ChristopheHerreman and #MattB are still right about encapsulation should not be broken.
But also JS Polymer elements access the parent in their layout elements because it's still convenient in some scenarios.
This works now in PolymerDart too.
Polymer.dart <= 0.16.x
(this.parentNode as ShadowRoot).host

Related

Making focus works inside a CK Editor 5 createUIElement

So I've a custom widget which renders a custom component.
conversion.for('editingDowncast').elementToElement({
model: 'modelName',
view: (modelElement, viewWriter) => {
const modelName = modelElement.getAttribute('modelName');
const modelNameView = viewWriter.createContainerElement('span', {
class: 'modelName',
'data-modelName': modelName,
});
const reactWrapper = viewWriter.createUIElement(
'span',
{
class: 'modelName__react-wrapper',
},
function (this, domDocument) {
const domElement = this.toDomElement(domDocument);
rendermodelName(modelName, domElement);
return domElement;
},
);
viewWriter.insert(
viewWriter.createPositionAt(modelNameView, 0),
reactWrapper,
);
return toWidgetEditable(modelNameView, viewWriter);
},
});
Where rendermodelName will give back a React component with a simple input box as
return (
<div>
<input type="text" />
</div>
);
https://ckeditor.com/docs/ckeditor5/latest/builds/guides/integration/frameworks/react.html.
But the problem is, whenever I tried to add some content inside the input, the focus is lost from the field and automatically moved to the surrounding editor. What am I missing. Tried creating a focushandler and adding the modelNameView to it.
Should I go with the new createRawElement? My current CK5 is 20.0.0 So I don't want any breaking changes coming now.
EDIT:
I researched a little bit more. seems like createRawElement may not work here. I think this doesn't have a simple solution. I tried with allowContentOf: '$block' which also not letting me focus. But these values are explicitly for normal CK widget, not for a react component.
I had the same issue and solved it by adding this tag to the parent div that wraps my Vue component.
https://ckeditor.com/docs/ckeditor5/latest/framework/guides/deep-dive/ui/widget-internals.html#exclude-dom-events-from-default-handlers
Adding from CKE Docs:
Sometimes it can be useful to prevent processing of events by default handlers, for example using React component inside an UIElement in the widget where, by default, widget itself wants to control everything. To make it possible the only thing to do is to add a data-cke-ignore-events attribute to an element or to its ancestor and then all events triggered by any of children from that element will be ignored in default handlers.
Let’s see it in an short example:
<div data-cke-ignore-events="true">
<button>Click!</button>
</div>
In the above template events dispatched from the button, which is placed inside containing data-cke-ignore-events attribute, will be ignored by default event handlers.
I faced the similar issue.
CKEditor will takes all the events on React component which you hosted on Widget.
The work around is to stop propagation of events to CKEditor which are fired from your DOM element(domElement) where your React component hosted.
Here is the sample code:
https://github.com/ckeditor/ckeditor5-core/compare/proto/input-widget#diff-44ca1561ce575490eac0d660407d5144R239
You should stop all required events. Also you can't paste any content inside the input field of React component. That will also listened by clipboardInput event of CKEditor.

Use scrollToAnchor from ViewportScroller in angular 6

I cannot figure out how to use the function ViewportScroller.scrollToAnchor(string anchor).
First of all - how do I define an anchor in my html? I may be confusing anchors, routerlinks and fragments.
My code which is based on fragments as of now:
export class ItemsOverviewPage implements OnInit {
public items: Item[];
constructor(private route: ActivatedRoute,
private scroller: ViewportScroller) {}
public async ngOnInit(): Promise<void> {
const fragment = await this.route.fragment.first().toPromise();
if (fragment !== undefined || fragment !== null) {
this.scroller.scrollToAnchor(fragment);
}
}
}
The html is something like
<ion-card mode="md"
*ngFor="let i of items"
routerDirection="forward"
id="{{ i.title) }}">
</ion-card>
How can I refer to the id? Or should I do an <a>...</a> around whatever I want to scroll to?
I am navigating to the page like:
this.router.navigate(['/items'], { fragment: item.title });
I don't think you can do it that way.
It seems that, when you are using a ngFor, the scrolling to the anchor gets called before the DOM is finalized.
So, in your ngOnInit you can get the fragment, but won't be able to find the anchor, as the ngFor has not completed yet.
One way way to do what you want, could be to use a route parameter rather than a fragment.You can retrieve the parameter in the ngOnInit of your ItemsOverviewPageComponent and store it in a variable (e.g. _fragment), and then scroll to the anchor in the ngAfterViewInit() hook using document.getElementById(this._fragment).scrollIntoView();
Another option, could be using navigationExtras.
Even better, if you need to pass data and potentially complex objects via routes, would be to set up a service that stores the data and then inject it into the components
For more information see ActivatedRoute, NavigationExtras

How to use d3 v4 with Polymer 2.0 classes?

How can d3.js (v4) be used with a Polymer 2.0 element?
Or how to use a library inside a class that already extends another class?
Trying to create a polymer d3 element to take advantage of polymer's two-way data binding and d3's syntax and functions. So that the data can be bound to a polymer property and passed to the d3.data() function?
Currently declaring d3 in the class returns undefined. Does the class need to be instantiated with d3 as a parameter? It seemed to work with Polymer 1.0. Another approach was to create a function outside of the class and call that but it's ugly. It would be nice to just use d3 inside the class.
Or is there a cleaner better way?
eg.
<script src="../../bower_components/d3/d3.js"></script>
var d3 = d3;
debugger; // hits this breakpoint first, and d3 is defined here and below
<dom-module id="my-app">
<template>
<svg id="svg"></svg>
</template>
<script>
class MyApp extends Polymer.Element {
static get is() { return 'my-app'; }
static get properties() {
return {
data: {
type: Object,
observer: '_dataChanged'
}
}
}
ready: {
var d3 = d3; // when it breaks here on the above breakpoint this is defined
debugger; // when it hits this breakpoint 2nd, d3 is undefined here and outside the class; what happened? how to scope it in?
}
_dataChanged(newValue, oldValue): {
var circle = d3.select(this.$.svg).data(newValue).enter().append(circle); //d3 undefined(! how to define?)
}
}
window.customElements.define(MyApp.is, MyApp);
</script>
</dom-module>
window.d3 is the way as you're loading the script in the global scope. You can load any external script asynchronously or synchronously.
To load it synchronously, just place the <script> tag in the <head>.
To load it asynchronously, you can add an load event listener to the <script> tag to do subsequent stuff when the load is completed.

Dynamically adding custom elements to DOM Aurelia [duplicate]

It seems Aurelia is not aware when I create and append an element in javascript and set a custom attribute (unless I am doing something wrong). For example,
const e = document.createElement('div');
e.setAttribute('custom-attr', 'some value');
body.appendChild(e);
Is there a way to make Aurelia aware of this custom attribute when it gets appended?
A little background: I am creating an app where the user can select their element type (e.g. input, select, checkbox etc.) and drag it around (the dragging is done in the custom attribute). I thought about creating a wrapper <div custom-attr repeat.for="e of elements"></div> and somehow render the elements array, but this seemed inefficient since the repeater will go through all the elements everytime I push a new one and I didn't not want to create a wrapper around something as simple as a text input that might be created.
You would have to manually trigger the Aurelia's enhance method for it to register the custom attributes or anything Aurelia related really. And you also have to pass in a ViewResources object containing the custom attribute.
Since this isn't as straight forward as you might think, I'll explain it a bit.
The enhance method requires the following parameters for this scenario:
Your HTML as plain text (string)
The binding context (in our scenario, it's just this)
A ViewResources object that has the required custom attribute
One way to get access to the ViewResources object that meets our requirements, is to require the custom attribute into your parent view and then use the parent view's ViewResources. To do that, require the view inside the parent view's HTML and then implement the created(owningView, thisView) callback in the controller. When it's fired, thisView will have a resources property, which is a ViewResources object that contains the require-d custom attribute.
Since I am HORRIBLE at explaining, please look into the example provided below.
Here is an example how to:
app.js
import { TemplatingEngine } from 'aurelia-framework';
export class App {
static inject = [TemplatingEngine];
message = 'Hello World!';
constructor(templatingEngine, viewResources) {
this._templatingEngine = templatingEngine;
}
created(owningView, thisView) {
this._viewResources = thisView.resources;
}
bind() {
this.createEnhanceAppend();
}
createEnhanceAppend() {
const span = document.createElement('span');
span.innerHTML = "<h5 example.bind=\"message\"></h5>";
this._templatingEngine.enhance({ element: span, bindingContext: this, resources: this._viewResources });
this.view.appendChild(span);
}
}
app.html
<template>
<require from="./example-custom-attribute"></require>
<div ref="view"></div>
</template>
Gist.run:
https://gist.run/?id=7b80d2498ed17bcb88f17b17c6f73fb9
Additional resources
Dwayne Charrington has written an excellent tutorial on this topic:
https://ilikekillnerds.com/2016/01/enhancing-at-will-using-aurelias-templating-engine-enhance-api/

Aurelia - multiple Enhance statements

Updated with solution (28.03.2017):
http://aurelia.io/hub.html#/doc/article/aurelia/framework/latest/app-configuration-and-startup/8
Have updated Aurelia docs with solution (scroll down a little).
Special thanks to Charleh for hint.
Question:
Aurelia has this nice feature calls enhance, which can help you enhancing specific parts of your application with Aurelia functional.
But can we have multiple enhance statements on the same page? It seems problematical.
Example:
Task: enhance first component on the page, then get some data from the server and enhance second component on the page with server data as binding context
HTML
<!DOCTYPE html>
<html>
<head>
<title>Title</title>
</head>
<body>
<my-component1></my-component1>
<my-component2></my-component2>
</body>
</html>
JS
import { bootstrap } from 'aurelia-bootstrapper-webpack';
bootstrap(function(aurelia) {
aurelia.use
.standardConfiguration()
.globalResources("my-component1", "my-component2");
aurelia.start().then((app) => {
// Enhance first element
app.enhance(null, document.querySelector('my-component1'));
// Get some data from server and then enhance second element with binding context
getSomeDataFromServer().then((data) => {
app.enhance(data, document.querySelector('my-component2'));
});
});
});
Result:
In the result we will enhance first component, but when it's time for the second one, Aurelia will try to enhance first component one more time!
It happens because of aurelia-framework.js _configureHost method.
So basically when you start enhance it starts this method with your element as an application host:
Aurelia.prototype.enhance = function enhance() {
var _this2 = this;
var bindingContext = arguments.length <= 0 || arguments[0] === undefined ? {} : arguments[0];
var applicationHost = arguments.length <= 1 || arguments[1] === undefined ? null : arguments[1];
this._configureHost(applicationHost || _aureliaPal.DOM.querySelectorAll('body')[0]);
return new Promise(function (resolve) {
var engine = _this2.container.get(_aureliaTemplating.TemplatingEngine);
_this2.root = engine.enhance({ container: _this2.container, element: _this2.host, resources: _this2.resources, bindingContext: bindingContext });
_this2.root.attached();
_this2._onAureliaComposed();
resolve(_this2);
});
};
And inside the _configureHost we can see this if statement which is just checking if our app instance is already host configured then do nothing.
Aurelia.prototype._configureHost = function _configureHost(applicationHost) {
if (this.hostConfigured) {
return;
}
...
Problem
So the actual problem here is that any enhanced element automatically became an application host (root) and when you try to enhance another element with the same aurelia instance you will just end up enhancing the first element always.
Question
Is this some way around for the cases when I want to enhance several elements on the page?
There's a clue here:
this.root = engine.enhance({container: this.container, element: this.host, resources: this.resources, bindingContext: bindingContext});
this.root.attached();
The aurelia.enhance just wraps the TemplatingEngine instance's .enhance method.
You could just pull TemplatingEngine from the container and call .enhance on it passing the bindingContext since aurelia.enhance does just that (but adds the additional "host configure" step that you've already done via your first .enhance call).
So that bit might look like:
import { Container } from 'aurelia-dependency-injection';
let engine = Container.instance.get(TemplatingEngine);
engine.enhance({ container: Container.instance, element: document.querySelect('my-component2'), resources: (you might need to inject these too), bindingContext: someContext });
(disclaimer: I didn't test the above so it may not be accurate - also you probably need to pass the resources object in - you can inject it or pull it from the container - I believe the type is just Resources)
However - something to note: your my-component2 won't actually be a child of your host element my-component1. I'm not sure if that will cause issues further down the line but it's just a thought.
I'm still curious as to why you'd want to bootstrap an Aurelia instance and then have it enhance multiple elements on the same page instead of just wrapping all that server response logic inside the component's viewmodel itself?
Maybe you can give a bit more context to the reason behind this?
My workaround for this issue for now (thanks to Charleh for the clue):
import { bootstrap } from 'aurelia-bootstrapper-webpack';
import {TemplatingEngine} from "aurelia-framework";
let enhanceNode = function (app, node, bindingContext = null) {
let engine = app.container.get(TemplatingEngine);
let component = engine.enhance({container: app.container, element: node, resources: app.resources, bindingContext: bindingContext});
component.attached();
}
bootstrap(function(aurelia) {
aurelia.use
.standardConfiguration()
.globalResources("my-component1", "my-component2")
aurelia.start().then((app) => {
enhanceNode(document.querySelector('my-component1'));
enhanceNode(document.querySelector('my-component2'));
});
});
That way you can skip host configuration for the app and can enhance as many custom elements as you want on the page.

Resources