I am trying to make a Polymer element that is a simple box or container with 3 major elements: a header, body, and footer. The header and footer would be optional, but if they are there, I want to put stuff around them.
My question is how to put a wrapper element around a <content> insertion point that only appears if the select attribute matches something?
My code is something like this (doesn't work):
<polymer-element name="example-card" attributes="">
<template>
<paper-shadow z="1">
<div vertical layout>
<div class="card card-header" hidden?="{{!$.header}}">
<content id="header" select=".header"></content>
</div>
<div class="card card-body">
<content id="body" select=":not(.footer)"></content>
</div>
<div class="card card-footer" hidden?="{{!$.footer}}">
<content id="footer" select=".footer"></content>
</div>
</div>
</paper-shadow>
</template>
<script>
Polymer({});
</script>
</polymer-element>
Expected use - no footer in this example:
<example-card>
<div class="header">Header...</div>
<div>Body...</div>
</example-card>
I either want to hide the wrapping div if its content is not selected, or put it in a <template if="..."> to get rid of it from the light DOM altogether. The hidden? expressions above aren't working.
I've tried a few different things with no success, including:
hidden?="{{!$.footer.getDistributedNodes().length}}"
Is there any way to conditionally wrap insertion points depending if their selection matched? Perhaps I am going about this all wrong. Thanks!
EDIT: thanks to Fuzzical Logic's help, this is what my revised version looks like, which uses lodash for collection filtering to only select header and footer classes of immediate children:
<polymer-element name="example-card" attributes="">
<template>
<paper-shadow z="1">
<div vertical layout>
<template if="{{showheader}}">
<div class="card card-header">
<content id="header" select=".header"></content>
</div>
</template>
<div class="card card-body">
<content select=":not(.footer)"></content>
</div>
<template if="{{showfooter}}">
<div class="card card-footer">
<content id="footer" select=".footer"></content>
</div>
</template>
</div>
</paper-shadow>
</template>
<script>
Polymer({
domReady: function () {
// Doesn't work in IE11...
// this.showheader = this.querySelector(':scope > .header');
// this.showfooter = this.querySelector(':scope > .footer');
this.showheader = _.filter(this.children, function(e) { return e.className === "header"; }).length;
this.showfooter = _.filter(this.children, function(e) { return e.className === "footer"; }).length;
},
publish: {
showheader: {
value: false,
reflect: true
},
showfooter: {
value: false,
reflect: true
}
}
});
</script>
</polymer-element>
The problem is not that the hidden? are not working. It's that $.header always resolves to the header with the id === "header" (i.e. this.$.header). This means it will always be truthy. The easiest way to fix this is to set attributes in your domReady event that checks the children.
Polymer('example-card', {
... other element code ...
domReady: function() {
this.showheader = this.querySelector('.header');
this.showfooter = this.querySelector('.footer');
},
publish: {
showheader: {
value: false,
reflect: true
},
showfooter: {
value: false,
reflect: true
}
},
... other element code ...
});
In order to make this work correctly, adjust your element as follows:
<div class="card card-header" hidden?="{{!showheader}}">
<content id="header" select=".header"></content>
</div>
<div class="card card-footer" hidden?="{{!showfooter}}">
<content id="footer" select=".footer"></content>
</div>
Important Note
The above domReady code is merely an example and not complete and will find any .header in the tree starting from the content. The correct way to do this would be to check only the children.
Related
I have an iron-list which I add new items to while scrolling to the bottom using iron-scroll-threshold. That works fine.
What I also need is a general event which is fired when scrolling stops.
I need that to know whether the listed items (m.message) have been seen by the user, by checking which items are currently visible in the view-port after scrolling, and then marking them as "read".
<div class="container" on-content-scroll="_scrollHandler">
<iron-scroll-threshold id="threshold" scroll-target="mlist" lower-threshold="500" on-lower-threshold="_loadMoreData"></iron-scroll-threshold>
<iron-list items="[[messages]]" id="mlist" as="m">
<template>
<div>
<p>[[m.message]]</p>
</div>
</template>
</iron-list>
</div>
The handler _scrollHandler is never fired however.
What would be necessary to get an event after scrolling ends?
You need the style: overflow: auto on the div.container. This will make sure the scroll event will invoke.
I could not find any such event as content-scroll, but with the changes above you should be able to change your HTML to bind against the handler like: on-scroll="_scrollHandler".
To detect if scrolling has stopped, I'd recommend using Polymer.debounce to have the callback set the isScrolling state to false like:
app._scrollHandler = function (e) {
app.isScrolling = true
app.debounce('scroll-handler', _ => {
app.isScrolling = false
}, 20)
}
It works at the end by moving on-scroll="_scrollHandler" to the iron-list:
<div class="container">
<iron-scroll-threshold id="threshold" scroll-target="mlist" lower-threshold="500" on-lower-threshold="_loadMoreData"></iron-scroll-threshold>
<iron-list items="[[messages]]" id="mlist" as="m" on-scroll="_scrollHandler">
<template>
<div>
<p>[[m.message]]</p>
</div>
</template>
</iron-list>
</div>
With the function being:
_scrollHandler: function() {
this.debounce("markAsRead", function(e) {
console.log("debounce");
}, 500);
}
Edit:
In case the iron-scroll-threshold wraps the iron-list, you need to move on-scroll to the iron-scroll-threshold-element:
<iron-scroll-threshold on-scroll="_scrollHandler" id="threshold" on-lower-threshold="_loadMore">
<iron-list scroll-target="threshold">...</iron-list>
</iron-scroll-threshold>
I am trying to use an iron-list (and iron-scroll-threshold) within a app-header-layout with has-scrolling-region.
I generated the basic app layout with the polymer-CLI.
If I do not use has-scrolling-region on the app-header-layout and use "document" for scroll-target on the iron-list it kinda works. But with this solution the scrollbar belongs to the window and does not slide beneath the header and I obviously cannot get the nice "waterfall" behaviour that is usually associated with these kinds of layouts.
Therefore, I use has-scrolling-region on the app-header-layout, but what is the right way to pass the corresponding scoller to the scroll-target property of the iron-list?
<!-- Main content -->
<app-header-layout has-scrolling-region id="layout">
<app-header condenses reveals effects="waterfall">
<app-toolbar>
<paper-icon-button icon="menu" drawer-toggle></paper-icon-button>
<div title>Twiamo</div>
</app-toolbar>
</app-header>
<iron-pages role="main" selected="[[page]]" attr-for-selected="name" id="page">
<my-iron-list name="view1" scroll-target="[[_getScrollTarget()]]"></my-iron-list>
<my-view2 name="view2"></my-view2>
<my-view3 name="view3"></my-view3>
</iron-pages>
</app-header-layout>
I looked into the implementation of app-header-layout to find the right element. This expression actually yields me the right element and everything works fine.
_getScrollTarget: function() {
return this.$.layout.shadowRoot.querySelector("#contentContainer");
}
But there has to be a better, a right way? Grabbing into the shadow DOM of the app-header-layout is not exactly using "public interface"!
To complete the example, here my code for my-iron-list. My-iron-list wraps and iron-list, iron-scroll-theshold, and some dummy data provider stuff. The scroll-target on my-iron-list is just passed to the iron-list and iron-scroll-threshold within my-iron-list:
<dom-module id="my-iron-list">
<template>
<iron-list items="[]" as=item id="list" scroll-target="[[scrollTarget]]">
<template>
<div class="item">[[item]]</div>
</template>
</iron-list>
<iron-scroll-threshold
id="scrollTheshold"
lower-threshold="100"
on-lower-threshold="_loadMoreData"
scroll-target="[[scrollTarget]]">
</iron-scroll-threshold>
</template>
<script>
Polymer({
is: 'my-iron-list',
properties: {
page: {
type : Number,
value : 0
},
perPage: {
type : Number,
value : 100
},
scrollTarget: HTMLElement,
},
_pushPage: function() {
for (i = 0; i < this.perPage; i++) {
this.$.list.push('items', 'Entry number ' + (i+1+this.page*this.perPage));
}
},
_loadMoreData: function() {
this._pushPage();
this.page = this.page + 1;
this.$.scrollTheshold.clearTriggers();
}
});
</script>
</dom-module>
I have the same problem as you, for now the cleanest anwser I have was to use the app-header scrollTarget.
In your case move add an id to the app-header
<app-header condenses reveals effects="waterfall" id="header">
<app-toolbar>
<paper-icon-button icon="menu" drawer-toggle></paper-icon-button>
<div title>Twiamo</div>
</app-toolbar>
</app-header>
and then instead of
_getScrollTarget: function() {
return this.$.layout.shadowRoot.querySelector("#contentContainer");
}
just use the scrollTarget property
_getScrollTarget: function() {
return this.$.header.scrollTarget;
}
If you found out a better way let me know.
Cheers,
I struggled with the same issue. While I was using iron-scroll-target-behavior instead of iron-scroll-threshold, I still needed to pass a scroll-target reference to an element inside a app-layout-header.
If has-scrolling-region is true, app-header-layout sets the scroll-target to be an internal div with an ID of #contentContainer. You can target this div and pass it as the scroll-target to your iron-list.
You would just need to alter the _getScrollTarget function inside your original code.
_getScrollTarget: function() {
return this.$.layout.$.contentContainer;
}
Hope it helps!
If anyone is coming here for an answer in 2017, I'm just letting you know that the same issue persists in Polymer 2.0.
I was able to overcome the issue by having the following code in my app shell (eg. PSK's my-app.html):
First, put an id attribute of 'layout' on your app-header-layout element.
Next, add this to your Polymer class (in your my-app.html equivalent):
static get properties() {
return {
scrollTarget: HTMLElement,
}
}
ready() {
super.ready();
this.scrollTarget = this.$.layout.shadowRoot.querySelector("#contentContainer");
}
Then, pass in the property to a scroll-target attribute on your lazy-loaded pages:
<my-page scroll-target="[[scrollTarget]]"></my-page>
Finally, in your lazy-loaded pages (eg. my-page):
<iron-list scroll-target="[[scrollTarget]]"></iron-list>
...
static get properties() {
return {
scrollTarget: HTMLElement,
}
}
This isn't an ideal solution, but it works.
I have a large chunk of HTML in an ng-repeat that for certain elements has a container element and for others it does not. I'm currently achieving this with two ng-ifs:
<strike ng-if="elem.flag">
… <!-- several lines of directives handling other branching cases -->
</strike>
<div ng-if="!elem.flag">
… <!-- those same several lines copied-and-pasted -->
</div>
While this works, it means I have to remember copy-and-paste any edits, which is not only inelegant but also prone to bugs. Ideally, I could DRY this up with something like the following (inspired by ng-class syntax):
<ng-element="{'strike':flag, 'div':(!flag)}">
… <!-- lots of code just once! -->
</ng-element>
Is there any way to achieve a similarly non-repetitive solution for this case?
You can make such directive yourself.
You can use ng-include to include the same content into both elements.
Assuming the effect you desire is to have the text within your tag be striked through based on the condition of the elem.flag:
You could simply use the ng-class as follows
angular.module('ngClassExample', [])
.controller('elemController', Controller1);
function Controller1() {
vm = this;
vm.flag = true;
vm.clickItem = clickItem
function clickItem() {
// Toggle the flag
vm.flag = !vm.flag;
};
}
.strikethrough{
text-decoration: line-through
}
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<div ng-app='ngClassExample' ng-controller="elemController as elem">
<div ng-class="{strikethrough: elem.flag}" ng-click="elem.clickItem()">
element content should be sticked through: {{elem.flag}}
</div>
</div>
You can do it with a directive
module.directive('myFlag', function() {
var tmpl1 = '<strike>...</strike>';
var tmpl2 = '<div>...</div>';
return {
scope: {
myFlag: '='
},
link: function(scope, element) {
element.html(''); // empty element
if (scope.myFlag) {
element.append(tmpl1);
} else {
element.append(tmpl2);
}
}
};
});
And you just use it like:
<div ng-repeat="item in list" my-flag="item.flag"></div>
You could create a directive which will transclude the content based on condition. For tranclusion you could use ng-transclude drirective, in directive template. Also you need to set transclude: true.
HTML
<my-directive ng-attr-element="{{elem.flag ? 'strike': 'div'}}">
<div> Common content</div>
</my-directive>
Directive
app.directive('myDirective', function($parse, $interpolate) {
return {
transclude: true,
replace: false, //will replace the directive element with directive template
template: function(element, attrs) {
//this seems hacky statement
var result = $interpolate(attrs.element)(element.parent().scope);
var html = '<'+ result + ' ng-transclude></'+result+'>';
return html;
}
}
})
Demo Plunkr
You can also use ng-transclude :
Create your directive :
<container-directive strike="flag">
<!-- your html here-->
</container-directive>
Then in your directive do something like :
<strike ng-if="strike">
<ng-transclude></ng-transclude>
</strike>
<div ng-if="!strike">
<ng-transclude></ng-transclude>
</div>
I'm trying to learn Polymer and I'm stuck. My goal is to put a list of checkboxes in the drawer panel on the left using core-list (the choices are dynamic, the server sends JSON in the page [no AJAX in this case]). Here's what I have:
<core-scaffold>
<core-header-panel navigation fit mode="seamed">
<core-toolbar>Left Header</core-toolbar>
<div fit style="overflow:auto;">
<core-list id="list" data="{{CheckboxList}}" flex multi>
<template>
<div class="row {{ {selected: selected} | tokenList }}">
Go
List row: {{index}}, Name: {{model.Name}}, Title: {{model.Title}}
<input type="checkbox" checked="{{model.Selected}}">
</div>
</template>
</core-list>
</div>
</core-header-panel>
<div tool>Right Panel Title</div>
<div vertical layout>
stuff...
</div>
</core-scaffold>
The div wrapping the list was intended to get rid of the following error, without success:
core-list must either be sized or be inside an overflow:auto div that is sized
I've wrapped this list every way I can think of to give it a size, and can't seem to shake this error message.
I'm also concerned because the docs said that core-list doesn't render the entire list, only a visible view of elements. This is a problem for checkboxes, since this needs to eventually submit a form with the checked values. Is there a way to override this functionality and force it to render the entire list, or is there an alternative to core-list that is better suited for repeating elements that must be rendered?
You may try using style="height:100%" as a core-list attribute. I recall Rob Dodson mentioning that the core-list explicitly needs to be given a height. By doing so I could get rid of the error you mentioned. Here is the jsfiddle and snipped:
http://jsfiddle.net/kreide/zt5xmoa9/
<link rel="import" href="http://www.polymer-project.org/components/polymer/polymer.html">
<link rel="import" href="http://www.polymer-project.org/components/paper-elements/paper-elements.html">
<link rel="import" href="http://www.polymer-project.org/components/core-elements/core-elements.html">
<polymer-element name="my-element" constructor="" attributes="">
<template>
<style>
.selected {
background: silver;
}
</style>
<core-scaffold>
<core-header-panel navigation fit mode="seamed">
<core-toolbar>Left Header</core-toolbar>
<div fit style="overflow:auto;">
<core-list id="list" data="{{CheckboxList}}" style="height:100%;" flex multi>
<template>
<div class="row {{ {selected: selected} | tokenList }}">
Go
List row: {{index}}, Name: {{model.Name}}, Title: {{model.Title}}
<input type="checkbox" checked="{{model.Selected}}">
</div>
</template>
</core-list>
</div>
</core-header-panel>
<div tool>Right Panel Title</div>
<div vertical layout>
stuff...
</div>
</core-scaffold>
</template>
<script>
Polymer('my-element', {
CheckboxList: [{"Name": "1", "Title": "1" }, {"Name": "2", "Title": "2" }, {"Name": "3", "Title": "3" }]
});
</script>
</polymer-element>
<my-element></my-element>
PS: I have no answer to your last question though, as I am not sure about rendering behaviour of core-list. core-menu might be an alternative as it is derived from core-selector and might act different from core-list. But these are just guesses, not an answer.
I think in response to your last question, you should bind the checkbox's check state to an attribute on the data used to render the core-list. This allows the core-list to separate its presentation from the state of the values that you keep in the list. Then, you can use that data in your ECMAscript model to perform actions or submit to a URI.
The demo for core-list shows data-binding to a number of different kinds of input elements.
I had the same issue and my solution was pretty simple. For:
<section route="home" id="home">
<div fit class="div-core-list">
<core-list data="{{data}}" flex>
<template>
<div class="row">
<div>{{model.name}}</div>
</div>
</template>
</core-list>
</div>
</section>
I've used the css code:
.div-core-list {
overflow: auto;
height: 300px; /* can be any value in px*/
}
core-list {
height:100%;
}
I hope it helps.
in every angular template we have to define a root html node, then inside it we can define the Html of our directive.
is there a way in angular to ignore that root node?
example :
my directive template :
<div class="space consuming div, and absolute positioning breaker">
<div class="content positioned relative to the directives parent 1"></div>
<div class="content positioned relative to the directives parent 2"></div>
<div class="content positioned relative to the directives parent 3"></div>
</div>
can we just set our template to be
<div class="content positioned relative to the directives parent 1"></div>
<div class="content positioned relative to the directives parent 2"></div>
<div class="content positioned relative to the directives parent 3"></div>
thanks!
You only need one root element if you are using replace: true in your template.
This is the case if you have defined custom element and are using then in your HTML in the following way:
<tabs>
<pane>1</pane>
<pane>2</pane>
</tabs>
In this case, replacing tabs with a template which has two roots will cause some confusion.
However, if you do not need replace: true, then you can set the directive on the element you want and assign a multi-root template on it. That template will be rendered inside the element which has the directive.
JS
var app = angular.module('plunker', []);
app.directive('myDirectiveOne', function() {
return {
template: '<p>Hello</p><p>World!</p>'
};
})
app.directive('myDirectiveTwo', function() {
return {
template: '<p>Hello</p><p>World!</p>',
replace: true
};
})
Template
<!-- works -->
<div my-directive-one></div>
<!-- has problem -->
<div my-directive-two></div>
E.g. http://plnkr.co/edit/jgEWsaxzfD4FkHcocJys?p=preview