Aurelia - Watch Dependency Value for Change - aurelia

Suppose you have a class you are injecting into a another class or component. Is there a way to watch for changes on an attributed of the dependency you are injecting and act upon it?
For example, say you have the following app:
app.html
<template>
<input type="text" value.bind="item">
<button click.trigger="addToList()">Add</button>
<h3>Modded</h3>
<ul>
<li repeat.for="it of modded">${it}</li>
</ul>
<h3>Original</h3>
<ul>
<li repeat.for="it of dep.items">${it}</li>
</ul>
</template>
app.js
import {bindable, inject} from 'aurelia-framework';
import {Dep} from './dep';
#inject(Dep)
export class App {
constructor(dep) {
this.dep = dep;
}
attached() {
this.modifyItems();
}
addToList() {
this.dep.addItem(this.item);
}
modifyItems() {
this.modded = [];
for (let item of this.dep.items) {
this.modded.push(item.toUpperCase());
}
}
}
dep.js
export class Dep {
constructor() {
this.items = ['one', 'two', 'three'];
}
addItem(item) {
this.items.push(item);
}
}
Now, let's say that some other component modifies Dep.items. Is there a way to watch for changes in app.js on this.dep.items and then call modifyItems()?
Assume modifyItems() is more complex than this example so maybe a value converter is not the best option. (unless it is the only option I guess)
Here is working plunker with the above example: http://plnkr.co/edit/rEs9UM?p=preview

Someone pointed me to the BindingEngine.collectionObserver and it appears that is what I needed.
app.js:
import {inject} from 'aurelia-framework';
import {BindingEngine} from 'aurelia-binding';
import {Dep} from './dep';
#inject(Dep, BindingEngine)
export class App {
constructor(dep, bindingEngine) {
this.dep = dep;
let subscription = bindingEngine.collectionObserver(this.dep.items)
.subscribe((newVal, oldVal) => {
console.debug(newVal, oldVal);
this.modifyItems();
});
}
attached() {
this.modifyItems();
}
addToList() {
this.dep.addItem(this.item);
this.item = '';
}
modifyItems() {
this.modded = [];
for (let item of this.dep.items) {
this.modded.push(item.toUpperCase());
}
}
}
Here is the working pluker: http://plnkr.co/edit/Pcyxrh?p=preview

Related

Vue 3 does not update getter

I am in the process of updating vue2 to vue3 but encounter this problem.
I have a service called TService
// T.ts
class T {
public obj = { value: false };
constructor() {
setInterval(() => {
this.obj.value = !this.obj.value;
}, 1000);
}
}
const t = new T();
export { t as TService };
The service is very simple, it update it's obj value every 1 second.
Now come to the fun part
On vue2, I can do this:
<template>
<div> {{ test }} </div>
</template>
<script lang="ts">
import { Component, Prop, Vue } from "vue-property-decorator";
import { TService } from './T;
#Component
export default class HelloWorld extends Vue {
public obj = TService.obj;
get test() {
return this.obj.value;
}
}
</script>
The test value updated on screen every 1sec and works as expected.
However, when I changed to vue3 with the below code, it does not work any more
<template>
<div>{{ test }}</div>
</template>
<script lang="ts">
import { Options, Vue } from "vue-class-component";
import { TService } from './T';
#Options({})
export default class HelloWorld extends Vue {
public obj = TService.obj;
get test() {
return this.obj.value;
}
}
</script>
Not sure what is going on and appreciate if anyone can fix my code.
I am using latest vue 3.1.5 and vue-class-component 8.0.0-rc.1
You probably should make it reactive so Vue knows its value can be updated, see here
The anwser can be found in this post:
Changes made to an object created outside of Vue component are not detected by Vue 3
Basically I will need to wrap reactive around my object in my service
import { reactive } from 'vue';
// T.ts
class T {
public obj = reactive({ value: false });
constructor() {
setInterval(() => {
this.obj.value = !this.obj.value;
}, 1000);
}
}
const t = new T();
export { t as TService };

Angular undefined value of #input variable

I'm new to Angular and I have some issues , hope you'll help me.
so I'm trying to share a value of a variable from a ProjectComponent to an AcceuilComponent , the value of this variable is displaying correctly into my acceuil.component.html but when I try to use it into my acceuil.component.ts it's undefined !
project.component.html (the parent component)
<app-header-in></app-header-in>
<ng-sidebar-container>
<ng-sidebar [opened]="opened">
<p> Sidebar </p>
<button (click)="Sidebar()">
Close Sidebar
</button>
<ul class="menu">
<li class="hh"
*ngFor="let project of projects"
[class.selected]="project === selectedProject"
(click)="onSelect(project)">
{{project.nomprojet}}</li>
</ul>
</ng-sidebar>
<div ng-sidebar-content >
<br><br><br><br>
<button (click)="Sidebar()">
Open Sidebar
</button>
<app-acceuil [message]="idProject"></app-acceuil>
</div>
</ng-sidebar-container>
project.component.ts
import { Component, OnInit } from '#angular/core';
import { ApiService } from '../api.service';
import {ProjectService} from '../project.service';
import {PartService} from '../part.service';
#Component({
selector: 'app-project',
templateUrl: './project.component.html',
styleUrls: ['./project.component.css']
})
export class ProjectComponent implements OnInit {
opened=true;
projects:any;
idProject;
selectedProject;
constructor(private projectService:ProjectService) { }
ngOnInit(): void {
this.projectService.getProjects().subscribe((result)=>{console.log("result",result)
this.projects=result})
}
Sidebar(){
this.opened=!this.opened;
}
onSelect(pro): void {
this.idProject = pro.id;
}
}
acceuil.component.html (my child component)
<p>{{message}}</p>
<ul >
<li class="hh"
*ngFor="let part of parts">
{{part.nomparti}}
</li>
</ul>
acceuil.component.ts
import { Component, OnInit,Input } from '#angular/core';
import { ApiService } from '../api.service';
import {PartService} from '../part.service';
#Component({
selector: 'app-acceuil',
templateUrl: './acceuil.component.html',
styleUrls: ['./acceuil.component.css']
})
export class AcceuilComponent implements OnInit {
#Input() message;
parts:any;
constructor(private partService:PartService) {
}
ngOnInit(): void{
console.log("id",this.message);
this.partService.getPartsFromIdProject(this.message).subscribe((result)=>{console.log("result",result)
this.parts=result})
}
ngOnChanges() {
if(this.message) {
console.log(this.message)
}
}
}
I'm using the message to call a service and displaying data .
in the acceuil.component.html <p>{{message}}</p> is displaying correctly but console.log("id",this.message); in acceuil.component.ts displays undefined
As message is an input property, you need to get its value in ngOnchanges life cycle.
First time, when it is in ngOnChanges, input value will be undefined. So for the safe side, better to add a check for not undefiled condition like below
ngOnChanges(changes: SimpleChanges) {
if (changes.message) {
// Should be defined now
console.log(this.message);
}
}

How to pass value from template HTML to component to then be used in service

I want to fetch id from component.html into component.ts to pass it to a service.
.ts file is;
import { Component, OnInit } from '#angular/core';
import { HttpClient } from '#angular/common/http'
import { HttpErrorResponse } from '#angular/common/http/src/response';
import { SendUsingApiService } from '../send-using-api.service';
import { Router, ActivatedRoute } from '#angular/router';
import { FormBuilder, FormGroup, Validators } from '#angular/forms';
import { setDefaultService } from 'selenium-webdriver/chrome';
#Component({
selector: 'app-org-info',
templateUrl: './org-info.component.html',
styleUrls: ['./org-info.component.css'],
providers: [SendUsingApiService]
})
export class OrgInfoComponent implements OnInit {
orgData: string[] = [];
Id = 1;
editRecord:FormGroup;
constructor(private httpService: HttpClient, private _serv: SendUsingApiService,
private fb: FormBuilder, private _ar:ActivatedRoute, private _r:Router) {
this.editRecord = this.fb.group({
Id:['1', []],
OrganisationName:['', []],
ContactPerson:['', []],
ContactPersonHPNo:['', []],
ContactPersonEmailId:['', []]
});
}
ngOnInit() {
console.log(this._ar.snapshot.params.Id, "+ve");
this._ar.params.subscribe(() => {
this._serv.getUsers(this._ar.snapshot.params.Id).subscribe((res)=>{
console.log(res);
this.setUser(res);
});
});
}
I am getting the value for console.log(this._ar.snapshot.params.Id); as undefined "+ve".
I want to get the Id value in console.
As per requests I am adding html part, though little adjusted;
<td style="text-align: center;">
<a class="btn btn-basic" [routerLink]="['/org-info',data['Id']]" role="button" (click)="getOrgData(data.Id)">View</a>
</td>
I defined a property instead of Id = 1; (above)
paramId = '';
then, within ngOnInit;
ngOnInit() {
this.paramId = this._ar.snapshot.params.Id;
console.log(paramId, "+ve");
}
Doing this, I got the Id value instead of undefined.

Is that the right way to set an id in Vue.js component?

I am trying to integrate Phaser 3 with Vue.js 2.
My goal is to create a Vue.js component associated with a game canvas.
My initial solution was:
<template>
<div :id="id">
</div>
</template>
<script>
import Phaser from 'phaser'
export default {
data () {
return {
id: null,
game: null
}
},
mounted () {
this.id = 'game' + this._uid
var config = {
parent: this.id,
type: Phaser.CANVAS
}
this.game = new Phaser.Game(config)
....
}
}
</script>
This code attach the game canvas to my template. But to my surprise it only worked 'sometimes'.
I figured out, after hours of debugging, that my div element in the DOM wasn't updated with the id when I was instantiating my new Game.
So I came up with the solution of instantiating the id in the beforeMount () method as follow:
<template>
<div :id="id">
</div>
</template>
<script>
import Phaser from 'phaser'
export default {
data () {
return {
id: null,
game: null
}
},
beforeMount () {
this.id = 'game' + this._uid
},
mounted () {
var config = {
parent: this.id,
type: Phaser.CANVAS
}
this.game = new Phaser.Game(config)
....
}
}
</script>
It is working, but I would like to know if there is a more simple and elegant solution ?
One better solution for integrating Phaser.Game into the application is directly passing the config the HTML element, a configuration supported by Phaser.Game.
To get a reference to a HTML element in vue, you can use refs, these are basically id's, but local to the component itself, so there is not risk in creating conflicts.
<template>
<div ref="myDiv">
</div>
</template>
<script>
import Phaser from 'phaser'
export default {
data () {
return {
game: null
}
},
mounted () {
var config = {
parent: this.$refs.myDiv,
type: Phaser.CANVAS
}
this.game = new Phaser.Game(config)
....
}
}
</script>
Vue3 sample:
<script setup>
import { ref,onMounted } from 'vue';
import Phaser from 'phaser'
const myDiv = ref(null)
let canvasWidth = 750;
let canvasHeight = 1450;
onMounted(() => {
const config = {
type: Phaser.AUTO,
parent: popWrap.value,
width: canvasWidth,
height: canvasHeight,
scene: {
preload: preload,
create: create,
update: update
}
};
const game = new Phaser.Game(config);
})
</script>
<template>
<div ref="myDiv">
</div>
</template>

"attached" or DOM-render equivalent for nested view-model.ref

We have a page, "parent", which references a template via the view-model.ref called "child" in the parent.html. We change the data of this child template by clicking on items on the parent page which invokes the child function using OpenDetailsDiv. Say I use a button for this event like below:
parent.html
<child view-model.ref="clientusertasks"></child>
<input type="button" value="Click Me" click.trigger="OpenDetailsDiv" />
In this manner we can invoke a function on the "child" view-model from the parent view-model like so:
parent.js
import { inject } from 'aurelia-framework';
import { HttpClient } from 'aurelia-fetch-client';
import 'fetch';
import AuthService from 'AuthService';
import { BpoClientUserTasks } from './bpo-client-user-tasks';
#inject(HttpClient, AuthService, BpoClientUserTasks)
export class Parent {
smallDivObj = {};
freq = '';
period = '';
filterVal = '';
client = '';
constructor(http, AuthService, BpoClientUserTasks) {
http.configure(config => {
config
.withBaseUrl("WebServices.asmx/")
.withDefaults({
headers: {
'Accept': 'application/json'
}
});
});
this.http = http;
this.auth = AuthService;
this.clientusertasks = BpoClientUserTasks;
}
OpenDetailsDiv(myObject) {
this.clientusertasks.CallBPOClientUserService(this.freq, this.period, this.filterVal, myObject.TrueClient, myObject.Client);
}
}
All good so far. The "child" view-model has this function CallBPOClientUserService which looks like the following:
child.js
import { inject } from 'aurelia-framework';
import { HttpClient } from 'aurelia-fetch-client';
import 'fetch';
import AuthService from 'AuthService';
#inject(HttpClient, AuthService)
export class Child {
smallDivObj = {};
constructor(http, AuthService) {
http.configure(config => {
config
.withBaseUrl("WebServices.asmx/")
.withDefaults({
headers: {
'Accept': 'application/json'
}
});
});
this.http = http;
this.auth = AuthService;
}
attached() {
}
CallBPOClientUserService(freq, period, filterVal, client, displayClient) {
$('#TasksByClientByUserDiv').addClass("fade");
this.freq = freq;
this.period = period;
this.filterVal = filterVal;
this.client = client;
var mymethod = {
method: 'post',
body: JSON.stringify({
"session": this.auth.session,
"Client": client,
"FreqFilter": freq,
"FilterVal": filterVal
}),
headers: {
'content-type': 'application/json'
}
};
//alert(JSON.stringify(mymethod));
this.http.fetch('GetBPOTasksByClientByUserDiv', mymethod)
.then(response => response.json())
.then(info => {
this.tasksByClientByUser = JSON.parse(info.d);
//setTimeout("$('#TasksByClientByUserTable').tablesorter();", 100);
});
}
}
Notice that in the function CallBPOClientUserService we wish to call a tablesorter sort function to sort our table in the view AFTER the DOM has been rendered.
Usually I would call upon this function in the "attached" component lifecycle of the view-model. But you can see that the manner in which we are populating this view is from the view-model.ref of the "parent" page which renders the "attached" component for "child" useless for this case (it's only called once after all when the parent is loaded).
So to my question:
Is there an equivalent attached-like component that I tap into to call this tablesorter function?
I have a cheap work-around where I can use a setTimeout which I have commented in the function, but I'd rather do this correctly in Aurelia events and have a guarantee that the DOM has finished.
I believe I have 2 solutions to this problem which I'm satisfied with and will post them here.
The first is the recommendation above by Fabio to use the microTaskQueue.
Another solution is to use a custom bindable event to invoke the function on completion of the repeat.for on the table here...
<template>
<require from='../tablesorter-bind'></require>
<section id="TasksByClientDiv" class="SmallDivPanel ui-draggable BIBulletinSection100 SmallDivSection hellfire">
<small-div-header smalldivinfo.bind="smallDivObj"></small-div-header>
<div id="TasksByClientBox">
<div style="margin-top: 10px;font-size: 20px;">Frequency: ${freq} / Period: ${filterVal}</div>
<div id="TasksByClientTableDiv" class="SmallDivMainPanel">
<table id="TasksByClientTable" >
<thead class="tablesorter">
<tr>
<th>Client</th>
<th>Received</th>
<th>Prepped</th>
<th>Loaded</th>
<th>Doc Loaded</th>
</tr>
</thead>
<tbody>
<tr click.trigger="OpenDetailsDiv(x)" repeat.for="x of tasksByClient" table-id.bind="tableId">
<td>${x.Client}</td>
<td>${x.totLoaded}</td>
<td>${x.totLoaded}</td>
<td>${x.totPrepped}</td>
<td>${x.numDocLoaded}</td>
</tr>
</tbody>
</table>
</div>
</div>
</section>
</template>
where tableId is defined in the View-Model as my tableID
I then setup the custom element like so:
tablesorter-bind.js
import {inject, customAttribute, TaskQueue} from 'aurelia-framework';
#customAttribute('table-id')
#inject(Element, TaskQueue)
export class TablesorterBind {
constructor(element, taskQueue) {
// "element" will be the DOM element rendered from the template
this.element = element;
this.taskQueue = taskQueue;
}
attached() {
}
bind(bindingContext, overridingContext) {
if (overridingContext.$last === true) {
this.taskQueue.queueMicroTask(
() => {
//This is the jQuery update call to the tablesorter function
$('#' + this.value).trigger('update');
}
);
}
}
}