Closed LordZardeck closed 9 years ago
We don't support inheritance at this time. Consider using composition, perhaps through a mixin implementation for now. We will consider enabling inheritance at a later time.
I would like to hear more details about how you think this should work.
@EisenbergEffect Well the idea is that, in the case above, the Deck
is just zone that still knows how to accept cards sent to it, but will utilize a different view, and in addition, the Deck
class could override specific Zone
methods to handle them differently. For example, a Discard
zone might override the _acceptCard
method to unshift
the card into it's internal array.
Exactly why doesn't inheritance work? isn't it just a JavaScript class? And if I can't use inheritance, what am I supposed to do? What do you mean by "using composition"?
The framework does some fancy-ness under the hood around bindables. We haven't done the work necessary to make it work with inheritance. By "composition" I mean creating a second class that has the shared implementation details of both the components and then merging that into each of the other classes.
@EisenbergEffect So essentially you are saying I have to have a Zone
class that defines the shared functionality, then have a ZoneElement
class that extends Zone
and DeckElement
that extends Zone
?
First thing's first, we will enable inheritance. It just has some issues now, so eventually what you want to do should work out of the box.
Inheritance of Bindables doesn't work. So, you could create a base class with all the shared parts, except the bindables. Then inherit from that and declare the bindables on each subclass. Also, bear in mind that you need a constructor for any inherited class and it needs to call super. That's an ES6 thing.
@EisenbergEffect So you are saying that I'll never be able to inherit a base class' bindables? It's something that's not possible?
I'm not saying "never". I'm saying that right now it doesn't work. We have a bug that we plan to address, probably post-beta. Right now, you can't inherit the bindables.
Oh, okay. That's fine. I thought you were saying it just wasn't possible. I can understand hacks right now since it's not even Beta. I was just worried it would never work properly.
What about nested components? Say, something like below
<DockFrame>
<DPanel Dock="Top" >
<CustomCompoent />
</DPanel>
<DPanel Dock="Bottom" />
<DockFrame Dock="Fill" >
<DPanel Dock="Top" />
<DPanel Dock="Bottom" />
</DockFrame>
</DockFrame>
DockFrame and DPanel are inherited from BasePanel which are used to implement a layout component. I have looked through the documents but can't find a good way to implement this.
@chongqingxiao what do You want to accomplish with BasePanel? If it is just a simple ES6 class, that includes common behaviour for both classes, then You can have it.
But the thing to note (as Rob mentioned above), is that You can't have bindable fields on parent class. In addition don't try to annotate it with @customComponent and expect html with the same file name to be used as a view for it.
If both subclasses should have the same view, you could annotate subclasses with @useView("./common-view.html")
- but i guesss mostly you don't want/need common view and You'll probably find componsition to be more appropriate for most cases.
@chongqingxiao like @atsu85 said, your BasePanel
class can't have any @bindable
or @customElement
decorators. You'll have to specify that on your DockFrame
and DPanel
classes. Other than that, you should just be able to:
import {customElement, bindable} from 'aurelia-framework';
export class BasePanel {
/* shared functionality */
}
@customElement('DockFrame')
export class DockFrame extends BasePanel {
@bindable Dock;
}
Got it. One more question, how the parent DockFrame can read the property of "Dock=Top" from its children "DPanel"? Or there is a better way to do children layout?
Thanks again for the quick response. Really appreciate it.
@chongqingxiao If the bindable property Dock
is available on all BasePanel
children, you should be able to access it in the BasePanel
parent. Just don't specify Dock
as a bindable. For example:
export class BasePanel {
dock;
myMethod() {
this.dock = 'foobar';
}
}
@customElement('DockFrame')
export class DockFrame extends BasePanel {
@bindable dock;
}
This is to follow up on my previous question in a slightly different way. Instead of extending custom element, what I am trying to to figure out is how to customize a child custom element.
Here is the user case,
I have a <tree-view>
and <tree-node>
The tree-view just renders a tree of tree-node using repeat-for plus a lot of layout related stuff.
The way how tree-view is used in most case is like below
<tree-view nodes.bind="nodes"></tree-view>
In above case, the tree-view will use tree-node to render nodes as string for each node.
The question I have is how do I replace part of tree-node so the UI for each node is different depending on the node. I know this can be done if it is a single level but I can't figure out how to do this for child customer element especially with binding.
The mock-up below is what I am trying to archive.
`
or even better
Update
below is the code for tree-view that using tree node content property but it doesn't work under repeat. If I remove the repeat div, the content view shows up correctly.
<template>
<require from='tree-node'></require>
<div>
<h2>A tree</h2>
<div repeat.for="node of nodes">
<content select="tree-node"></content>
</div>
</div>
</template>
You can not use a content
element as part of a template controller's template (ie if or repeat). The content
element is part of the v0 shadow dom spec and must be static...meaning it can't be dynamically added or removed and you can't have multiple copies of it.
I'd need to understand your scenario better in order to advise you of what path to take. To me, even looking at the markup above...it doesn't make any sense, based on how the content element works. So, I need to know a bit more to help you.
Hi, Rob:
Thanks for the response.
Let me clarify the problem. Let's say I released a tree-view component which by default use tree-node to render individual node as string in the tree.
To make it easier for the consumer of the tree-view component to customize the tree, I want to expose some part of the tree-node so that part can be extended or replaced.
In WPF, a HeaderTemplate or TreeViewItemTemplate can be defined so part of the tree-node can be extended. I am trying to figure out a similar way to do so in Aurelia.
One solution I come up with is to add a tree-node-view custom attribute like above so the view for the tree-node can be replaced by the users of the tree-view components. Something like below
<tree-view nodes.bind="nodes" tree-node-view="NewTreeNodeView.html">
</tree-view>
I don't like this though since the extension point seems limited. I was hoping for something like below
<tree-view nodes.bind="nodes">
<tree-node-template>
???Customization goes here - say add an image to each node???
</tree-node-template>
</tree-view>
But I can't figure out how to implement that. Hope this clarifies my question.
A unrelated question is what is the best way to either post question or asking for enhancement since I figured this is not the best place to ask questions. :)
Thanks again for taking time to answer the question.
Ok, for that you need to use template part replacement. See http://aurelia.io/docs.html#template-parts
I just noticed that and plan to try it out. :)
I promise this is my last question on this :)
I am trying to write a tabs component which is implemented in angular 2 as in the following link write a tabs component using angular
Here is how the tabs will be used
page.html
<template>
<tabs>
<tab tab-title="Tab 1">
Here's some content.
</tab>
<tab tab-title="Tab 2">
And here's more in another tab.
</tab>
</tabs>
other contents
<template>
I try to follow the same pattern to get the "tabs" parent from tab.js using bind bind(parent : any) { this.parent = parent; }
The problem is the parent i got is not tabs, it is the view object that contains tabs
page.html
<template>
<tabs>
<tab tab-title="tabA"></tab>
</tabs>
</template>
In the above example, tab.js -> bind -> context is the page object instead of the tabs object.
Two questions 1) For nested components as above, should the bind parameter be the first nested parent instead of the grand parent? 2) What is the best pattern to implement the tabs component like above? Essentially, when each child tab is added to tabs, I need to access the tab element to get the tab-title so I can use it to display the tab header inside tabs component. In the angular 2 example, when tab is constructed, it calls addTab of tabs object to insert it self into tabs object. Ideally, I was hoping there is a custom children binding event where after children tab is parsed and constructed, tabs.js can access the tab instance to do what ever is needed - in this case, get the tab-title. Another useful case would be to limit the type of children can be inserted - say tabs will only tab as the first level children.
Sorry for the long post.
Good question. There are a couple of things to mention here:
You could implement this very similarly to Angular 2, by injecting the Tabs into each Tab and then having the Tab add itself to the Tabs' collection. However, the Angular 2 example has a serious bug because of this approach. Is assumes that the order that the tabs are created in is the order that they should be displayed in and that the order of the tabs will never change. This won't be true if the tabs are generated by a repeater or bound to an array where the item order changes. So, you don't want to build it that way in either Angular 2 or in Aurelia. There's also another issue with the Angular 2 implementation. The non-selected tabs only have their div content hidden when they aren't selected, so the tab element itself will remain visible in the dom. If that element has any styling, it will continue to affect layout (and any styles will be visible) even if the tab is hidden. So, that won't work correctly. In Aurelia, we solve this using a surrogate behavior. By placing the show behavior directly on the template, it will affect the element itself.
tabs.js
import {sync} from 'aurelia-framework';
export class Tabs {
@sync('tab') items = [];
selectedItem = null;
//sync properties gets change callbacks, just like bindables
///In this case, we just want to ensure that there is always at least one tab selected/visible.
itemsChanged(mutations) {
if (this.items.length > 0) {
if (!this.selectedItem || this.items.indexOf(this.selectedItem) === -1) {
this.selectItem(this.items[0]);
}
}
}
//manage the selected tab
selectItem(item) {
if (this.selectedItem) {
this.selectedItem.isSelected = false;
}
this.selectedItem = item;
if (this.selectedItem) {
this.selectedItem.isSelected = true;
}
}
}
tabs.html
<template>
<ul class="nav nav-tabs">
<li repeat.for="item of items" class="${item.isSelected ? 'active' : ''}">
<a click.trigger="$parent.selectItem(item)" replaceable part="header">${item.title}</a>
</li>
</ul>
<content select="tab"></content>
</template>
tab.js
import {bindable} from 'aurelia-framework';
export class Tab {
@bindable title;
constructor(){
this.isSelected = false;
}
}
tab.html
<template show.bind="isSelected">
<div class="tab-pane">
<content></content>
</div>
</template>
Probably the most interesting part here is the @sync
property. This allows you to specify a css selector and then the framework will synchronize that property with the child DOM elements that match that selector. So, your tabs collection will always match the number and order of tabs in the dom, regardless of how or when they were created.
Note: This example is based on bootstrap styles, so if you have bootstrap, it will style appropriately.
Bonus: I showed how you can make the tab header a "replaceable part" so that consumers of the tab control can optionally provide their own template to customize the rendering of the tab heads. So, for example, if you wanted to have a close button in the tab header, to close the tab, you could override the header part and provide your own template for the header to do that.
You can use it as follows:
<tabs>
<tab title="My Static Tab">
Here's some static content.
</tab>
<tab repeat.for="item of dynamicTabsArray" title.bind="item.title">
<compose view-model="${item.type}"></compose>
</tab>
</tabs>
Note: This example is significantly more advanced than the Angular 2 scenario. This demonstrates statically defined tabs, mixed with tabs generated by a repeater from a data source. The content of the dynamic tabs is itself dynamically composed based on the data.
Note: Don't forget to "require" the custom elements in your view or else make the global :smile:
That doesn't answer my question. My question is how to implement the tabs control so that it can get the title attribute from the static tab children.
<tabs>
<tab title="my static tab header">
</tabs>
I understand I can get the title if i use dynamic tabs array where each item has a "title" property but I want the tabs control to support static tab children as well.
That's exactly what @EisenbergEffect said above:
<tabs>
<tab title="My Static Tab"> <!-- Here -->
Here's some static content.
</tab>
<tab repeat.for="item of dynamicTabsArray" title.bind="item.title">
<compose view-model="${item.type}"></compose>
</tab>
</tabs>
Does that not cover your requirement?
No. for each static tab (not dynamic bound one) in the tabs control, the tabs need to get the "title" part and uses it as the tab header for each tab content. Essentially, I need a way to get all the tab children static defined inside tabs control In the angular 2 example, the tab children adds itself to its parent when it is constructed and then inside tabs template, it loops the added tabs to repeat the title as the tab header. I was trying to figure out how to do that using aurelia. Again, this is for static defined a list of tab inside tabs component when it is used - not dynamic bounding one. I guess a slightly different question is how tabs control can get a reference of all the children tab defined statically when it is used.
<template>
<ul>
<li *ng-for="#tab of tabs" (click)="selectTab(tab)">
{{tab.tabTitle}}
</li>
</ul>
<content></content>
</template>
Below is my code
tab.html
<template>
<section>
//here is the code to handle visibility based on which tab header is active
<content></content>
</section>
</template>
tabs.html
<template>
//ul is used to render tab header for each tab using tab-title and it is when I get stuck for static defined tab children
<ul>
<li>
???
</li>
</ul>
<content></content> //this is for all tab content
</template>
To simplify, assuming I have the following component
export class Tabs {
tabs: [];
constructor() {
this.tabs = [];
}
}
how I can populate this.tabs inside Tabs component given tabs is used in the following way
<tabs>
<tab title="tab1></tab>
<tab title="tab2></tab>
</tabs>
In the angular 2 example, this.tabs is populated through tabs.addTab method called by tab children when children is constructed through DI. I was trying to do the similar thing through code below inside Tab.js
bind(parent : any)
{
this.parent = parent;
this.parent.addTab(this);
}
But the parent is the grand parent which host tabs instead of tabs which doesn't work.
But that's exactly what that does:
<tabs>
<tab title="My Static Tab"> <!-- Here -->
Here's some static content.
</tab>
<tab repeat.for="item of dynamicTabsArray" title.bind="item.title">
<compose view-model="${item.type}"></compose>
</tab>
</tabs>
I've even marked it with an HTML comment Here
unless I'm missing something. The @sync
decorator in the example takes care of selecting the DOM elements and populating an array when the DOM mutates
This ensures that anything you create (be it static or dynamic HTML) is captured by the tab control container - this array is then iterated using repeat.for
to create the actual visualisation of the tabs.
It's probably not clear from the example but @sync
has the following sig (if I'm not mistaken - check the source in aurelia/templating-resources)
@sync(selector, propertyName, changeHandler)
The 3 must be strings and specify the CSS selector used to get the DOM elements, the property name to create on the owning VM and the callback that fires when the array mutates.
(the default values for propertyName
and changeHandler
are "items" and "itemsChanged" respectively, hence the omission in the example)
If the elements that are selected are backed by Aurelia viewmodels, the array contains the viewmodel instances, otherwise it contains the element itself.
In this case, the array will contain viewmodels for all <tab>
custom elements in the view (they are "transcluded" into the DOM of the parent custom element using the content
selector)
This array is then iterated over.
Basically you are flipping it on it's head vs the ng2 example - the container looks for the tabs and tracks them rather than the tabs adding themselves to the container.
Let me know if any of this isn't clear, or just implement Robs example and it should become more apparent
@sync is exactly what I am looking for. :) Is there an example you can point me to? I did a quick search but can't find how it is being used.
Super thanks for the quick response even on weekends. Your guys rock.
The best pre-written example I have is the one above.
I found one good example. example
I really hope your guys can come up with blog posts or books that cover more advanced topic 1) Example of complex component such as tab/tree/grid 2) How binding/template update works internally - For example, if an element was added or removed from array, how the corresponding DOM element is updated. Also how cascaded updates are handled. There are quite a few post on how angular 2 change detection and template update works internally but I haven't been able to find any post on aurelia.js 3) Plans for tools integration. I am trying aurelia using visual studio code and it is pretty difficult to figure out errors when things are not working. For example, if I spell template wrong or missing require, it just doesn't work. I am not sure if that is because I am using TypeScript but any tool integration that helps with template syntax check and debug will be super.
Thanks for the wonderful work and I really enjoyed playing with it for my side project.
@chongqingxiao I've got a few examples to answer 1) they may be a little out of date and most are just proof of concept exercises for my own understanding but here they are:
Using Kendo grid with Aurelia (supporting Aurelia templates in a Kendo grid) https://gist.github.com/charlespockert/1ef96e44f3f15ab60f5e
Tab set example (very similar to Rob's example but minus the surrogate behaviours - I don't think they were in at the time I wrote the example or I didn't know about them yet) https://gist.github.com/charlespockert/54228f77a06fce04c8d3
An attempt at a native Aurelia grid control (only supports simple things like sorting, paging, remote+local data, column templates etc - nothing fancy) Demo: http://charlespockert.github.io/aurelia-bs-grid-demo/ Repo: https://github.com/charlespockert/aurelia-bs-grid
Hope these help out
2) and 3) I'll leave to Rob (I'm not an Aurelia team member), but I can say that I'm using TS and sometimes just plain ES6 and it helps at the moment to just double check everything until the tooling catches up :)
@charlespockert Thanks a lot for the examples. As for tools integration, I personally think that is going to have a big impact on the adoption of aurelia js. I know aurelia is still working in progress but it will be super to hear from Rob on what is the direction and the future plan for better tools integration.
I posted the question on gitter but it seems it is the limitation of how dom selector works for nested components. With @sync, the selector doesn't work with nested components - say tabs inside tabs
For example, with
<tabs>
<tab title="tab1"></tab>
<tabs>
<tab title="tab2"></tab>
</tabs>
</tabs>
in tabs.js @sync -> selector "tab" will return both tab1 and tab2 instead of just tab1. "tabs > tab" returns the same result.
Should I submit a bug or enhancement somewhere?
We could make it so that it only matches direct children. That wouldn't be hard. Making it so that it doesn't cross a component boundary, without native shadow dom support, would be very difficult. Would selecting only immediate children that match the selector be good enough?
@EisenbergEffect I suppose you could support full selectors in the @sync
decorator but only as long as you were happy using a shim to support the :scope
pseudo-selector (IE doesn't have native support but Chrome and FF do at the moment) - otherwise some options are off limits
You could provide the option of using node.matches()
or alternatively using querySelectorAll()
when mutations are tracked and then discard/accept splices by intersecting the matches with the mutations.
It might not be as performant as the node.matches()
code but would give the developer much more control over how child elements were selected - I can see people wanting to nest tab controls and the like and at the moment some scenarios are not achievable
So it would look more like:
@sync({ name: "tabs", selector: ":scope > tab" })
Ensuring you only get direct children
@charlespockert Great idea! I actually didn't now about the :scope
selector. Do you know if there's a good shim out there we could use to fix up IE?
Just tried ":scope > tab". Chrome works but safari doesn't work. I didn't get a chance to test IE. If there is a shim out there that can fix up IE, I will be super happy. :)
To make it more flexible, maybe add a filter function? In asp.net, it has something like CreateChildControls which lets the container to control the children constructions. Something similar might be useful as well.
@EisenbergEffect @chongqingxiao there is a shim availble:
https://github.com/lazd/scopedQuerySelectorShim
API could remain the same too as you can detect more complex selectors automatically using regex.
Something like: ^\s*?[a-zA-Z0-9\-]{1,}\s*?$
to find the simple element ones, other ones must be more complex
Modifies HTMLElement.prototype Document.prototype's querySelector/querySelectorAll methods are not shimmed :scope is not relevant at the document level Use document.documentElement.querySelector instead without :scope
Shim looks sensible too
@charlespockert Would you mind creating a new issue to track this problem and linking the above resource in?
@EisenbergEffect issue added, I've added as much detail as possible:
Thank you! On Oct 18, 2015 7:27 AM, "Charles Pockert" notifications@github.com wrote:
@EisenbergEffect https://github.com/EisenbergEffect issue added, I've added as much detail as possible:
aurelia/templating#191 https://github.com/aurelia/templating/issues/191
— Reply to this email directly or view it on GitHub https://github.com/aurelia/framework/issues/210#issuecomment-149004266.
@charlespockert , @EisenbergEffect Thanks your guys to resolve this.
Any updates on inheriting @bindable or could someone provide a code example of how this can be accomplished using a mixin
It is announced in Early October 2017 Release in Aurelia blog that:
Allow inheritance of bindable properties for custom elements in aurelia-templating
v1.5.0. But I could not find any code example.
I tried to put up a minimal gist to test this, but it didn't work: https://gist.run/?id=0128b19d4121e93611321a0240a6b75b
What is missing here?
So i’m having a fun issue where I extend a custom element, which then removes the extended custom element. For example. I have a generic “zone” class with the
@customElement
decorator. I then extend this “zone” class with a “deck” class. All my other zones are then no longer available.Zone.js
Deck.js