I want to use a java-script method in a polymer Template. I am using Vaadin with Polymer Elements. In my Project I have a Vaadin-Grid of Objects that can be of different type. I want to render these types with different Templates.
This problem can be solved with a dom-if template, as described by ollitietavainen in this answer
This works perfectly, but there is a problem. When using more than two different Types of Objects in the Grid, one would need to use the same amount of booleans to set that up. Suppose we have a fictional shop that displays PC-Parts, and each type of PC-Part needs to be rendered with its own template, then we would need something like the fallowing. This is quite cumbersome.
private boolean isMemory(AbstractPcPart pcPart) {
return pcPart.getClass().equals(Memory.class);
}
private boolean isGraphicsCard(AbstractPcPart pcPart) {
return pcPart.getClass().equals(GraphicsCard.class);
}
private boolean isCPU(AbstractPcPart pcPart) {
return pcPart.getClass().equals(CPU.class);
}
// … is-checker for all other types of pcParts.
private void initColumn() {
addColumn(Objects.requireNonNull(CardFactory.getTemplate())
.withProperty("partCard", CardFactory::create)
.withProperty("isMemory", this::isMemory)
.withProperty("isGraphicsCard", this::isGraphicsCard)
.withProperty("isCPU", this::isCPU)
// add all other properties
);
}
The corresponding Templates would look something like this.
<template is='dom-if' if='[[item.isMemory]]'>"
<memory-card part-card='[[item.partCard]]'>
</memory-card>"
</template>
<template is='dom-if' if='[[item.isGraphicsCard]]'>"
<graphics-card part-card='[[item.partCard]]'>
</graphics-card-card>"
</template>
<template is='dom-if' if='[[item.isCPU]]'>"
<cpu-card part-card='[[item.partCard]]'>
</cpu-card>"
</template>
<!-- one additional template for every type of part -->
The question now is, if there is any other way, that would not be needing all these Properties.
Luckily there is, as Kuba Šimonovský explained in an answer to another question.
Using this method we could rewrite the code from above to something like the fallowing.
private String type(AbstractPcPart pcPart) {
return pcPart.getClass().getSimpleName();
}
private void initColumn() {
addColumn(Objects.requireNonNull(CardFactory.getTemplate())
.withProperty("partCard", CardFactory::create)
.withProperty("type", this::type));
}
This time we use a java-script method to conditionally select the corresponding template.
<template is='dom-if' if='[[_isEqualTo(item.type, "Memory")]]'>"
<memory-card part-card='[[item.partCard]]'>
</memory-card>"
</template>
<template is='dom-if' if='[[_isEqualTo(item.type, "GraphicsCard")]]'>"
<graphics-card part-card='[[item.partCard]]'>
</graphics-card-card>"
</template>
<template is='dom-if' if='[[_isEqualTo(item.type, "CPU")]]'>"
<cpu-card part-card='[[item.partCard]]'>
</cpu-card>"
</template>
<!-- one additional template for every type of part -->
The Polymer Template is a bit more complicated now, but on the java side, the code is much shorter, and possibly easier to maintain. There is probably still some overhead, as every template gets added to the dom. But in addition to that only the content from the templates that we want to see gets added to the dom.
I don’t think there is a better way to do this though.
So using this method, we need a java-script method called _isEqualTo. This method is not a standard method so we need to implement it ourselves. The implementation for this method is straightforward.
function _isEqualTo(one, other) {
return one == other;
}
But the answer from Kuba does not specify where to implement this method. I have tried to put the method in different places with no luck. The js console in my browser always complains that it can not find the method.
Digging a little bit deeper I found this Link. So maybe what i want to have is a global variable.
window._isEqualTo = function(one, other) {
return one == other;
}
But even with this change the same warning persists. What’s weird is that the function is visible in the interactive console in the developer tools. Setting a breakpoint in the java-script file that i have added the function; and calling the function in the console reveals that it is really the correct function that get’s called, leading me to beleave that the function gets initialized too late in the lifecycle of the application. Although I am not sure at all.
And because the function is not found, the grid in the view will be empty. It still shows the rows, but they don’t show content.
I really hope someone can help me out.
Here is a Git-Repository to reproduce my problem. The concerning views are the PartsDomIfView and the PartsDomIfElegantView.
Instead of using the deprecated TemplateRenderer, you could create a LitRenderer (v22+) and create a custom lit component that can be used there as your column's content. In there you could create complex logic based templates as a separate component, that can be better maintained.
Related
We are migrating a medium sized app from polymer 1 to polymer 3. Thus far we are stuck in the intermediate step of getting our hybrid components to work.
We are encounting some difficulties regarding component initialization timing. For example:
<my-app>
<my-component slot='componentslot'><my-component>
</my-app>
It seems there are cases where my-component is initialized before my-app is initialized. It might vary wether my-component is part of shadow- or light-dom.
We have a lot of tightly coupled components which depend on deterministic initialization order. For example there is a tree-like structure where every edge and every leaf uses events to discover it's own depth in the tree. Therefore we need top-level elements to be initialized before inner components.
But what we found so far was essentially: There is no garantuee for any initialization order of the components.
Is there an established pattern for solving this problem? Will this problem be solved in polymer 3 (so we don't need to care about it anyway)?
Edit
I was asked for some more specific examples
Example 1
<my-layout>
<my-complex-component id="1">
<my-reuseable-part/>
</my-complex-component>
<my-complex-component id="2">
<my-reuseable-part/>
</my-complex-component>
<some-other-component>
<my-reuseable-part/>
</some-other-component>
</my-layout>
I have some reuseable components which need to know if they are inside my-complex-component orsome-other-component. my-complex-component uses a context-discovery-behavior which fires an event containing a callback as payload. my-complex-component and some-other-component have context-behaviors which listen to that event and answer it by invoking the callback.
But as my-reusable-part might be attached before my-complex-component or some-other-component is attached, this pattern does not work.
Registration of event listeners as well as firing the disovering event is done in attached (i.e. connectedCallback).
Example 2
<my-tree>
<my-tree-edge>
<my-tree-edge>
<my-leaf/>
<my-tree-edge>
<my-leaf/>
</my-tree-edge>
</my-tree-edge>
<my-tree-edge>
<my-leaf/>
</my-tree-edge>
<my-leaf/>
</my-tree-edge>
</my-tree>
In the example above every leaf and edge needs to know how deep it is nested. Again every elements fires an event and its parent will answer the event. Again listener registration and event-firing is done in attached/connectedCallback. Again the mechanik fails if an inner node is attached before it's parents are attached.
Hope this helps.
You can use dom-if element if you stricly want to be sure first render my-app then you can let render my-component something like:
<my-app ready="{{myAppReady}}>
<template is='dom-if' if="[[myAppReady]]">
<my-component slot='componentslot'><my-component>
</template>
</my-app>
at my-app script:
static get properties(){return {
ready:{type:Boolean,
notify:true,
value:false
}}
at this part, you may add computed:"checkThisValuesToBeSUre(x,[y]..) in order to be sure if depended to some values or you may add various conditions in order to render my-component
Also, you may import my-component.js dynamically like:
At my-app 's parent script:
static get observers(){return ['_checkMyAppReady(myAppReady)']}
_checkMyAppReady(r){
if(r) import('./my-component.js');
}
EDIT
If there are many elements occurs the same problem, then better to use lazy-import.js:
_checkMyAppReady(r){
if(r) import('./lazy-import.js');
}
lazy-import.js
import './my-component.js';
import './my-component2.js';
import './my-component3.js';
...
I have a problem with the new ng-content transclusion.
Let's say I have a component my-component that, in its ngOnInit() function does some heavy operation on load (for now, just a console.log()).
I have a wrapper, that displays the content via transclusion (my-wrapper.component.html).
<ng-content></ng-content>
If I set the surroundings up like this, the log statement doesn't show:
<my-wrapper *ngIf="false">
<my-component></my-component>
</my-wrapper>
I assume, the my-wrapper component does not get built, so the content is ignored.
But if I try to move the logic into the my-wrapper component like this (my-wrapper.component.html):
<ng-container *ngIf="false">
<ng-content></ng-content>
</ng-container>
I always see the console.log() output. I guess, the my-component gets built and then stored away until the *ngIf becomes true inside my-wrapper.
The intention was to build a generic "list-item + detail" component. Say I have a list of N overview-elements (my-wrapper), that get rendered in a *ngFor loop. Every of those elements has its own detail component (my-component) that is supposed to load its own data, once I decide to show more infos to a specific item.
overview.html:
<ng-container *ngFor="let item of items">
<my-wrapper>
<my-component id="item.id"></my-component>
</my-wrapper>
</ng-container>
my-wrapper.component.html:
<div (click)="toggleDetail()">Click for more</div>
<div *ngIf="showDetail">
<ng-content></ng-content>
</div>
Is there a way to tell Angular, to ignore the transcluded content until it is necessary to be added to the page? Analogously to how it was in AngularJS.
Based on the comment of #nsinreal I found an answer. I find it to be a bit abstruse, so I'm trying to post it here:
The answer is to work with ng-template and *ngTemplateOutlet.
In the my-wrapper component, set up the template like this (my-wrapper.component.html):
<div (click)="toggleDetail()">Click for more</div>
<div *ngIf="showDetail" [hidden]="!isInitialized">
<ng-container *ngTemplateOutlet="detailRef"></ng-container>
</div>
Note, that the [hidden] there is not really necessary, it hides the "raw" template of the child until it decides it is done loading. Just make sure, not to put it in a *ngIf, otherwise the *ngTemplateOutlet will never get triggered, leading to nothing happening at all.
To set the detailRef, put this in the component code (my-wrapper.component.ts):
import { ContentChild, TemplateRef } from '#angular/core';
#Component({ ... })
export class MyWrapperComponent {
#ContentChild(TemplateRef) detailRef;
...
}
Now, you can use the wrapper like this:
<my-wrapper>
<ng-template>
<my-component></my-component>
</ng-template>
</my-wrapper>
I am not sure, why it needs such complicated "workarounds", when it used to be so easy to do this in AngularJS.
By doing this:
<my-wrapper *ngIf="false">
<my-component></my-component>
</my-wrapper>
You are not calling MyComponent component, because the *ngIf is false. that means, that not calling it you are not instancing it and, therefore, not passing through its ngOnInit. And that's why you are not getting the console log.
By doing this:
<ng-container *ngIf="false">
<ng-content></ng-content>
</ng-container>
You are inside the component, you are just limiting what to render in your template, but you already instanced your component and, therefore, you passed through your ngOnInit and you get your console log done.
If, you want to limit something (component call with selector or a ng-content or even a div) until you have some data available, you can do the following:
datasLoaded: Promise<boolean>;
this.getData().subscribe(
(data) => {
this.datasLoaded = Promise.resolve(true); // Setting the Promise as resolved after I have the needed data
}
);
And in your template:
<ng-container *ngIf="datasLoaded | async">
// stuff here
</ng-container>
Or:
<my-component *ngIf="datasLoaded | async">
// Didn't test this one, but should follow the same logic. If it doesn't, wrap it and add the ngIf to the wrapper
</my-component>
It’s because Ng content happens at the build time and when you pass the content it is actually not removed or recreated with the ngIf directive. It is only moved and the component is instantiated .
I encountered this problem recently as well but settled on a different solution than the currently accepted one.
Solution (TL;DR)
(Solution is for AngularDart; I figure it's similar in Angular though)
Use a structural directive; tutorials linked below.
Instead of:
<my-wrapper>
<my-contents></my-contents>
</my-wrapper>
your usage becomes:
<div *myWrapper>
<my-contents></my-contents>
</div>
which is shorthand for the following (in AngularDart; I think Angular uses <ng-template>)
<template myWrapper>
<div>
<my-contents></my-contents>
</div>
</template>
The MyWrapper directive logic is similar to NgIf except it has its own logic to compute the condition. Both of the following tutorials explain how to create an NgIf-like directive and how to pass it your own inputs using the special microsyntax (e.g. *myWrapper="myInput: expression"). Note that the microsyntax doesn't support outputs (#Output), but you can mimic an output by using an input that is a function.
Tutorial for Angular
Tutorial for AngularDart
Caveat: Since this is just a directive, it shouldn't do anything more complicated than instantiating a template ref at the appropriate time and maybe specifying some DI providers. For example, I would avoid trying to apply styles or instantiating a complex tree of components in the directive. If I wanted to create a list component, I would probably take the #ContentChild(TemplateRef) approach described in another answer; you would lose the asterisk shorthand for creating <template> but you would gain the full power of components.
My problem
My team owns an app that's part of a larger web application with other apps owned by other teams. Our components assume they can inject a MyAppConfiguration object, but this object can only be injected after it is loaded with an asynchronous request. In our app this is not a problem: we have a "shell" component that hides everything behind an ngIf until the configuration is loaded.
The problem is when other teams want to reference our components. We don't want them to duplicate the "wait until configuration is loaded" logic every time, so I tried creating a wrapper component that can be used like so:
<my-app-wrapper>
<my-app-component></my-app-component>
</my-app-wrapper>
The wrapper injects a service object and hides its contents behind an ngIf until the service says that the configuration is loaded.
Like the question poster, I discovered that the ng-content approach doesn't work as intended: while the contents are correctly hidden from the DOM, Angular still instantiates the components causing dependency injection to fail.
The solution that I settled on was to rewrite the wrapper component as a structural directive.
I would like to bind attr and attr1 in the following example. These two parameters do not depend on data (other attributes do). When using the binding this way, there is only one shared data object between all 'rows'. I want only the two components of one iteration to be bound together.
<template repeat"{{data in dataList}}">
<component1 attr="{{binding}}" />
<component2 attr2="{{binding}}" />
</template>
My first idea was to bind the attributes to an variable of the data object:
<template repeat"{{data in dataList}}">
<component1 attr="{{data.binding}}" />
<component2 attr2="{{data.binding}}" />
</template>
This solution on the other hand is really ugly, because the model object get's view-only data attached. Because the model lives normally longer than the components, this could cause a huge overhead. Another problem is serialization, which could fail because of the attached data.
Is there any elegant solution? The only one I imagined so far is to wrap the data objects before iterating over the data set. This approach on the other hand would probably make problems with model updates...
Naive thought: Shouldn't be the scope of a variable that is only used inside of a template restricted to this template? In the special case of the repeat template furthermore to one iteration?
You might type-suggest your binding variable being an Object and use it like:
<polymer-element name="my-element" attributes="dataList">
<template>
<ul>
<template repeat="{{d in dataList}}">
<li>
{{d}} ⇒ {{binding[d]}} <!-- Since type-suggested, it works -->
</li>
</template>
</ul>
</template>
<script>
Polymer({
dataList: ['navy', 'maroon'],
binding: {} /* it’s necessary to type-suggest var for Polymer */
});
</script>
</polymer-element>
Please be aware that the snippet above expects different items in dataList. Live preview: http://plnkr.co/edit/ez36BVUPCKW8xRSkGxOM?p=preview
Naive thought: Shouldn't be the scope of a variable that is only used inside of a template restricted to this template? In the special case of the repeat template furthermore to one iteration?
This sounds impossible for me, because (besides that this will overcomplicate the implementation) sometimes one wants to bind the variable in nested template:
<template repeat="{{a in lst}}">
{{bound_here}}
<template id="nested">
{{bound_here}}
...
With what you suggested the binding above becomes impossible.
Let's say I have a Polymer element x-foo which uses templates for data-binding.
<template>
<!--shadow DOM-->
<template repeat='{{item in items}}'>
<div class='content'>{{item}}</div>
</template>
</template>
items is a property of x-foo which decides what is present in the view.
Now, on the fly in one of the methods of x-foo I do:
this.items = getNewItemList();
and then try to access shadow DOM content,
this.shadowRoot.querySelectorAll('.content') // was supposed to return 5 elements
I find that Polymer still hasn't iterated through the template loop and generated my shadow DOM content. Is there a way to know when it has finished it?
By design, Polymer waits until your JavaScript has finished processing before it does expensive things like messing with DOM. That way you can do several operations at once and not worry about thrashing DOM and slowing down your application.
The short answer to your question is to do something like this:
this.items = getNewItemList();
this.async(
// `async` lets the main loop resume and perform tasks, like DOM updates,
// then it calls your callback
function() {
this.contents = this.shadowRoot.querySelectorAll('.content');
}
);
A better answer is to avoid needing to query for the elements. Instead, let the elements communicate with the container via events or even using the 'item' objects (the data model). If you can drive your UI from your data-model, and not the reverse, you will have a better time.
I currently have a PolymerElement which is binding to a observable list, and using
<template repeat="{{cardnames}}">
<div>{{ }}</div>
</template>
So far, so good. The cardnames is populated from a IndexDB store, I'm using lawndart for this. It works for getting all the items at startup, but when I add a item to the database, from a separate PolymerElement, there is no way to update the cardnames list from this other PolymerElement. So one of the ideas I have come up with, without putting all this logic into the same PolymerElement, of having the one with the cardnames in it monitor the Database for changes, and update the list from there, when a change happens. My problem is I don't know if the is already a change event that can be listened to, and I was hoping someone could enlighten me of if there is, and where to find it or show me how to do it.
Of coarse I could run a background Isolate process to check for changes, or a timer of some sort, but that seems clumsy, costly and an increase in complexity I could do without.
Thanks
OK I'm going to answer my own question.
I think the answer to if there is a IndexedDB event that is triggered when an item is added, is no. I'd like to be proven wrong, but the only update event I've seen is for version updates, not added content updates. I shouldn't be surprised by this.
Anyway, I found that using the code below, unsurprisingly, works, though I'd have preferred to use an event generated by IndexedDB or something, but you don't always get what you want. This is done using Lawndart, which effectively uses IndexedDb in most modern browsers.
...
//define an observable to hold the list
#observable List<String> cardnames;
static const INTERVAL = const Duration(seconds: 1);
Timer poller;
...
poller = new Timer.periodic(INTERVAL, pollDB);
...
void pollDB(Timer timer) {
Store db = new Store("magic-card-collection", "magic-card");
db.open().then((_) {
db.keys().forEach((item) {
if(!cardnames.contains(item)) {
cardnames.add(item);
print("added " + item);
}
});
});
}
Now every time a item is added to cardnames, the view gets updated, dynamically as more are added to the DB. So I can now add cards from any outside source, and the view will be updated within seconds.
UPDATE:
With some help from both Seth Ladd and the Polymer documentation, I figured out the best way is not to poll the db, but to have one central DB store contained in one element, have the other elements fire custom events when they do something, passing the info as part of the fired event, and having the controller element listen for those events and both add to the store plus update the UI. For example create an element that collects data to put in the db, and have it fire an event when finished collecting that info.
<polymer-element name="collect-data">
<template>
<div>
<button on-click="{{save}}">Ok</button>
</div>
</template>
<script type="application/dart" src="collectdata.dart"></script>
</polymer-element>
The collectors dart file
#CustomTag('collect-data')
class CollectData extends PolymerElement {
CollectData.created() : super.created();
void save(Event e, var detail, var target) {
fire("update", detail: "hello");
}
}
The controller
<polymer-element name="app-controller">
<link rel="import" href="collectdata.html">
<template>
<div>
<collect-data on-update="{{save}}"></collect-data>
</div>
</template>
<script type="application/dart" src="appcontroller.dart"></script>
</polymer-element>
The app controller dart file
#CustomTag('app-controller')
class AppController extends PolymerElement {
AppController.created() : super.created();
void save(Event e) {
var data = e.detail;
//update your db here, which would, if you had one, include updating an observable list
...
}
}
The important things to note are the method
fire('update', detail: ...);
The detail can be any valid object, I think. I know you can pass a string or a dict to it. The other part is
<collect-data on-update="{{save}}"></collect-data>
The thing that fires the event calls it update, the listener listens to on-update. I'll leave it to you to figure out the pattern requirement here.
However the DB still doesn't emit a changed event that I know of at the moment.