I have custom Tabs functionality, but after the update of Aurelia to rc-1.0.x, there is issue with listing data from #children decorator.
My code look something like:
import {inject, customElement, children} from 'aurelia-framework';
#customElement('tabs')
#inject(Element)
#children({name:'tabs', selector: "tab"})
export class Tabs {
activeTab = undefined;
constructor(element) {
this.element = element;
}
attached() {
console.log(this.tabs); // Return undefined on promise resolve!!!
this.tabs.forEach(tab => {
if (tab.active) {
this.activeTab = tab;
}
tab.hide();
});
this.activeTab.show();
}
On first load everything is working just fine, and this.tabs is an array of items, as expected.
Next if I do a server request, when promise is resolved this.tabs console logs undefined.
If I set timeout it fix the issue, but is that the correct way?
Also I noticed in the html, that the repeat.for statement is executed, which give me a clue that this.tabs is received with some delay, after the attached function is handled.
The html:
<template>
<ul class="nav nav-tabs m-b-1">
<li repeat.for="tab of tabs">
<a href="#" click.trigger="$parent.onTabClick(tab)">
${tab.name & t}
</a>
</li>
</ul>
<slot></slot>
</template>
So is there a way to make that work with the Aurelia bind or attached methods or some more elegant way, instead to check the value of this.tabs with a timeout function?
I think this might be an adequate use-case for the Task Queue (but if someone more knowledgeable thinks otherwise, let me know). I have had to use the Task Queue before when accessing bindable values from inside of attached and your instance looks basically the same (except you're accessing them using #children).
The TaskQueue will push your logic to the end of the processing stack. So theoretically this means it will wait for Aurelia to finish running its internal logic for bindings and possibly children decorator resolution and then run your code.
There might be a better solution, but I have used this before to get out of a similar situation as I mentioned earlier and have yet to find a better solution that fixes the problem, specifically in regards to accessing dynamic values inside of attached.
import {TaskQueue} from 'aurelia-framework';
#customElement('tabs')
#inject(Element, TaskQueue)
#children({name:'tabs', selector: "tab"})
export class Tabs {
activeTab = undefined;
constructor(element, taskQueue) {
this.element = element;
this.taskQueue = taskQueue;
}
attached() {
this.taskQueue.queueMicroTask(() =>{
this.tabs.forEach(tab => {
if (tab.active) {
this.activeTab = tab;
}
tab.hide();
});
this.activeTab.show();
});
}
Related
To do something after the DOM has been updated by Vue, you use the $nextTick binding.
To do something after a css transition has completed, you can use transitionend event.
I have a dynamic list in which things are added and removed by user actions. When removed, there is a CSS animation and then I need to check the state of the DOM immediately after the element is gone.
I was thinking that the $nextTick after the transitionend would be the state of the DOM immediately after the list item is removed, but it is not.
I need to do something after the transition has ended and the element from a list has been removed from the DOM.
Right now I have:
<transition-group class="message-bus" tag="ul" name="message-bus">
<li v-for="message in messages" v-bind:key="message.id" :ref="message.id">
<div>{{message.text}}</div>
<a class="dismiss-message" #click="dismissMessage(message)">×</a>
</li>
</transition-group>
dismissMessage(message){
const vm = this;
this.$refs[message.id][0].addEventListener("transitionend", function(){
vm.$nextTick(function(){
//This is called, but appears to be called before the element is actually removed from the DOM
//I need to query the DOM immediately after this element is removed
});
});
this.messages.splice(this.messages.indexOf(message), 1);
}
In the mounted function, I have added a MutationObserver that appears to be working as needed. I'll put this here as an answer as it does technically work and may be helpful to others, but I'm still interested in a better answer if Vue has something built in for this.
mounted(){
const vm = this;
const listItemRemoved = new MutationObserver(function(e){
if (e[0].removedNodes.length){
console.log("Removed");
}
});
listItemRemoved.observe(this.$el, {childList: true});
}
Perhaps you could use a custom directive. Perform the actions you need inside the unbind hook ..
created() {
this.vm = this
},
directives: {
foo: {
unbind(el, binding) {
// Here you can perform the actions you need.
// You can access the Vue instance using binding.value (eg: binding.value.$el)
}
}
},
And in your template ..
<transition-group class="message-bus" tag="ul" name="message-bus">
<li v-for="message in messages" v-bind:key="message.id" :ref="message.id" v-foo="vm">
<div>{{message.text}}</div>
<a class="dismiss-message" #click="dismissMessage(message)">×</a>
</li>
</transition-group>
I want to call a child component's function from its parent. I have a way to do it, but I want to know if I'm missing a better way.
From Ashley Grant's blog post about accessing a custom element's viewModel from a custom attribute, I see that Aurelia adds au to the element and you can access the viewModel through that. So, if I add a nested component with a ref, like this:
<template>
<nested-element ref="childElement"></nested-element>
</template>
I can call a function on it like this:
this.childElement.au.controller.viewModel.someFunction();
This feels roundabout. I was hoping I would be able to access a nested element's viewModel through the parameters to a hook that the parent implements, such as created(owningView, myView) but I can't find a path to it.
Have I missed a better way?
Edit: I forgot to add that I need a return value from the function I'm calling, so having access to the viewmodel itself is preferable
ref gives you the element. view-model.ref gives you the element's view model.
<template>
<nested-element view-model.ref="childViewModel"></nested-element>
</template>
Call it like this in the parent view-model:
this.childViewModel.someFunction();
If you only have one instance of the nested-element or don't care if multiple nested-elements respond to the event. Then you could use standard Javascript event functionality for this:
bar.html
<template>
<h1>${value}</h1>
<input type="text" value.bind="value"></input>
<foo>text</foo>
</template>
bar.ts
import {bindable} from 'aurelia-framework';
export class Bar {
#bindable value;
public valueChanged(newValue, oldValue) {
var event = new CustomEvent("some-event-name", { "detail": { message: "Hello from Bar", oldValue, newValue } });
document.dispatchEvent(event);
}
}
foo.html
<template>
<h1>${value}</h1>
</template>
foo.ts
import {bindable} from 'aurelia-framework';
export class Foo {
constructor() {
document.addEventListener("some-event-name", (e) => {
console.log('hello here is Foo, recieved event from Bar : ', e);
}, true);
}
}
Almost give up on Aurelia, I'm struggling with adding custom elements dynamic in Aurelia,
lets say we have a custom tag:
view my-element.html:
<template> My Element ${name} </template>
viewmodel: my-element.js:
export class MyElement {
#bindable name = '';
}
so I try to manually add this tag, in another view:
<template>
<button type="button" click.delegate="createMyElement()">Remove</button>
</template>
another viewmodel:
export class App {
createMyElement() {
//how to do it in here to create element
//<my-element name='name1'></my-element>
}
}
I looked this link https://gist.run/?id=762c00133d5d5be624f9, but it needs a container reference
<div ref="container"></div>
I dont want to specify a container, instead I want it to be append to current view.
I also tried using aurelia-compiler from https://github.com/gooy/aurelia-compiler, when I try to import it, it was able to locate file'gooy/aurelia-compiler', but I got this error:
Error invoking Compiler. Are you trying to inject/register something that doesn't exist with DI?
Can someone please help? thanks.
You could inject the view's html element and use it as a "container". Like this:
import {inject} from 'aurelia-framework';
import {ViewFactory} from './view-factory';
#inject(Element, ViewFactory)
export class App {
//...
constructor(element, viewFactory) {
this.element = element;
this.viewFactory = viewFactory
}
}
Then, use this.element in the insert method:
this.dispose = this.viewFactory.insert(this.element, this.viewHtml, viewModel);
Running example:
https://gist.run/?id=9d5e7a60cd02e55618829a304df00621
Hope this helps!
Rather than trying to manually inject views through your viewModel (controller), try creating new viewModels from which to generate views. So, like this:
home.html
<template>
<my-element repeat.for="name of names" name.bind="name"></my-element>
<button click.delegate="addName()">Create My Element</button>
</template>
home.js
export class HomeViewModel {
constructor() {
this.names = []
}
addName() {
this.names.push('Jim');
}
}
I have an application that is closely tied to the DOM. I need to keep track of the size and position of the elements that represent the objects behind them.
myViewModel.js
export class MyViewModel {
// my root view model has important properties
// that all other functions and objects need to use
constructor() {
this.importantProperty = 'veryimportant';
this.things = [];
}
// i create things in the view model that are
// represented in the dom
createThing() {
this.things.push({
isAThing: true
});
}
// i do things with things in the view model
// that depend strongly on the root view model
doSomethingWithThing(thing, property) {
thing[property] = `${this.importantProperty}${property}`;
}
// but i need to know all about the dom representation
// of the things in the view model
doAnotherThingWithThing(thing) {
console.log(`the height of the thing is ${thing.height}`);
}
lookAndSeeWhatSizeThisThingIs(element, thing) {
thing.height = element.clientHeight;
thing.width = element.clientWidth;
console.assert('That was easy!');
}
}
myViewModel.html
<template>
<!-- these things can change in size and shape, and I have
no idea what they will be until runtime
<div repeat.for="thing of things"
<!-- so ideally I'd like to call something like this -->
composed.delegate="lookAndSeeWhatSizeThisThingIs($element, thing)">
<img src="img/${$index}.png" />
</div>
</div>
Is there a way to do this today?
Since a CustomAttribute has access to the composition lifecycle, we can create a CustomAttribute that triggers an event on the element that fires in the attached() callback.
import {autoinject} from 'aurelia-framework';
#inject(Element)
export class AttachableCustomAttribute {
constructor(element) {
this.element = element;
}
attached() {
this.element.dispatchEvent(
new CustomEvent('attached'));
}
}
And use it just like any other event binding, with the exception that it does not bubble, and thus we must use trigger instead of delegate.
<div repeat.for="thing of things"
attached.trigger="lookAndSeeWhatSizeThisThingIs($event, thing)" attachable>
<img src="img/${$index}.png" />
</div>
I was curious if I can get element properties form component template.
So I have made simple div with class and I've made this class:
export class viewApp{
elementView: any;
viewHeight: number;
myDOM: Object;
constructor() {
this.myDOM = new BrowserDomAdapter();
}
clickMe(){
this.elementView = this.myDOM.query('div.view-main-screen');
this.viewHeight = this.myDOM.getStyle(this.elementView, 'height');
}
}
getStyle(), query() are from BrowserDomAdapter.
My problem is when I try to get height it is null, but when I set some height by setStyle() and then I get it by getStyle() it returns proper value.
After checking DOM and styles in browser I discovered that is because of two CSS elements. One is: .view-main-screen[_ngcontent-aer-1]{} and second one is element{}.
.view-main-screen has some stylings, but element is empty. When I add styles by setStyle() it appears in element{}. Why is that? How can I get element properties by using Angular2?
The correct way is to use #ViewChild() decorator:
https://angular.io/docs/ts/latest/api/core/index/ViewChild-decorator.html
Template:
<div class="view-main-screen" #mainScreen></div>
Component:
import { ElementRef, ViewChild } from '#angular/core';
export class viewApp{
#ViewChild('mainScreen') elementView: ElementRef;
viewHeight: number;
constructor() {
}
clickMe(){
this.viewHeight = this.elementView.nativeElement.offsetHeight;
}
}
That should do it but obviously you need to add your Component decorator.
Edit:
For Angular 8 or later you need to provide the 2nd parameter in ViewChild
#ViewChild('mainScreen', {read: ElementRef, static:false}) elementView: ElementRef;
update2
constructor(private elementRef:ElementRef) {}
someMethod() {
console.log(this.elementRef.nativeElement.offsetHeight);
}
Accessing nativeElement directly is discouraged but current Angular doesn't provide other ways as far as I know.
update
https://github.com/angular/angular/pull/8452#issuecomment-220460761
mhevery commented 12 days ago
We have decided to remove Ruler service, and so it is not part of the public API.
original
As far as I know the Ruler class should provide that functionality
https://github.com/angular/angular/blob/master/modules/angular2/src/platform/browser/ruler.ts if this isn't enought you probably need to access elementRef.nativeElement and use direct DOM access and functions provided by the elements.
new Ruler(DOM).measure(this.elRef).then((rect: any) => {
});
Rules service is safe in WebWorker.
See also the comments on https://github.com/angular/angular/issues/6515#issuecomment-173353649
<div #getElementHeight>
Height
</div>
Height of element is {{ getElementHeight.offsetHeight }}
<div *ngFor="let item of items" (click)="itemClick($event.currentTarget)"></div>
itemClick(dom){
var height=dom.clientHeight;
// ...
}