Let's say I have an array of elements and in addition to displaying the list in my app, I want to sync the list to the server with HttpClient. How can I observe changes to the array? I tried:
#inject(ObserverLocator)
export class ViewModel {
constructor(obsLoc) {
this.list = [];
obsLoc.getObserver(this, 'list');
.subscribe(li => console.log(li));
}
}
But I got neither error nor log message.
getObserver returns a property observer which will notify you when the ViewModel class instance's list property changes. This will only happen when you assign a new value to the list property, ie this.list = [1,2,3]. If you're not assigning new values to the list property and instead are mutating the value of the property via push, pop, splice, etc, you'll want to use an array observer. Use the ObserverLocator's getArrayObserver method- it takes one parameter, the array you want to observe:
import {ObserverLocator} from 'aurelia-binding'; // or from 'aurelia-framework'
#inject(ObserverLocator)
export class ViewModel {
constructor(obsLoc) {
this.list = [];
obsLoc.getArrayObserver(this.list);
.subscribe(splices => console.log(splices));
}
}
October 2015 update
The ObserverLocator is Aurelia's internal "bare metal" API. There's now a public API for the binding engine that could be used:
import {BindingEngine} from 'aurelia-binding'; // or from 'aurelia-framework'
#inject(BindingEngine)
export class ViewModel {
constructor(bindingEngine) {
this.list = []; // any Array, Map and soon Set will be supported
// subscribe
let subscription = bindingEngine.collectionObserver(this.list)
.subscribe(splices => console.log(splices));
// be sure to unsubscribe **later**
subscription.dispose();
}
}
Related
I have custom objects for holding child objects full of data. The child objects are initiated with null values for all their properties, so the objects can be referenced and their properties filled from remote sources. This creates a lazy-loading setup.
This code is going to be extremely trimmed down, but everything relevant should be here:
class Collection extends Object {
constructor(){
this.loaded = false;
var allLoaders = [];
var loaderPropmises = [];
var resolver;
const $this = this;
var trackLoaders = function(){
$this.loaded = false;
loaderPromises.push(Promise.all(allLoaders).then(() => {
//... irrelevant logic in here to ensure only the latest promise sets loaded to true
$this.loaded = true; //This is getting called where I expect
resolver();
}));
}
//hook for outside things to watch the promise if they want
this.loader = new Promise((resolve) => {
//this only gets resolved once, which is fine
resolver = resolve;
});
//... bunch of code around adding child objects, but the important part:
this.add(child){
this[child.id] = child;
this.allLoaders.push(child.loader);
trackLoaders();
}
}
}
The child then looks like:
class Child extends Object {
constructor(){
this.loaded = false;
var resolver;
const $this = this;
this.loader = new Promise((resolve) => {
resolver = resolve;
}).then((){
$this.loaded = true;
});
this.populate(data){
//bunch of stuff to set data to properties on this object
resolver();
}
}
}
In Vuex 4 I have these Collections as properties on an "AppData" object in the store:
const store = createStore({
state: function(){
AppData: {}
},
mutations: {
setupCollection(state, name){
if (!Object.hasOwnProperty.call(state.AppData, name){
state.AppData[name] = new Collection();
}
}
},
actions: {
//this is called on each row of data returned from an Axios call
add (context, {name, data}){
context.state.AppData[name][data.id].populate(data);
}
}
});
The idea is that whenever a Child is added to a Collection, the collection loaded property will be false until all the Child loader promises resolve. This all executes perfectly... Except that the loaded bools aren't reactive.
Right now, I have a Promise.all in each component's Created function that flags the component as "loaded" once all the objects needed for the component have had their "loader" promises resolved. This absolutely works, but isn't ideal as different data will be available at different times, and there are sometimes hundreds or more of these classes on screen at once. What I'm trying to accomplish is:
<div v-if="!myCollection.loaded">
Loading...
</div>
<div v-else>
Show the data I want here {{myCollection.property}}
</div>
So I have two thoughts on overcoming this, either of which would be great:
VueJS3 no longer has a need for Vue.set(), because Proxies. How would I make the loaded bools here reactive then? Or more specifically, what am I doing that prevents this from working?
Alternatively, is there a practical way to use the loader promise directly in a template?
It looks like Vue's ref is what I needed:
this.loaded = ref(false);
This works, at least on the Child class. I have some sort of circular referencing issue going on and haven't been able to test on the Collection class yes, but it should work the same.
I'm using mobx-react / mobx-react-lite for state management
Using classes i can define a non observable idToDelete to store the clicked item id, open a Modal using an observable and when the user clicks "Delete", i know the item to delete. The id is "remembered" by the component trough the re-renders
class Greeting extends React.Component {
idToDelete = null;
confirmDelete = id => {
this.idToDelete = id;
openConfirm = true;
}
closeModal = () => {
openConfirm = true;
this.idToDelete = null;
}
#observable
openConfirm = false;
render() {
// List of items with delete button
<button onClick=this.confirmDelete(id)>Delete</button>
// Confirm Delete Modal
}
}
But in a stateless component the id will become null (the initialization value) on each re-render.
Using useLocalStore hook i can store observable values:
All properties of the returned object will be made observable
automatically
But i dont want to re-render just because i am storing/changing the id.
Using React.React.createContext / useContext seems like a bit overkill to me (it's kind of private value and it is not relevant outside the component itself)
Is there a "local storage" way to achieve this? (without observable convertion)
What are best practices for this situation?
You can use the useRef hook to save the value. A change to this value will not trigger a re-render and the value will remain the same across renders unless you override it.
Its also explained in detail here
Yes! The useRef() Hook isn’t just for DOM refs. The “ref” object is a
generic container whose current property is mutable and can hold any
value, similar to an instance property on a class.
eg:
import { useRef } from 'react';
const idToDelete = useRef("");
confirmDelete = id => {
idToDelete.current = id;
}
closeModal = () => {
idToDelete.current = null;
}
Also mind the catch, you need to use .current to access the data.
I have a Dto that I want to enable the service layer to filter:
The method selectFields takes an array of field names that should be returned, the other properties will be removed.
What is a short way to enumerate the properties on the class so I can loop through them and set the filtered ones to null?
In the BaseDto I take care of cleaning falsy values (well I need the same function here too as a matter of fact).
class UserServiceDto extends BaseDto {
constructor(userDto) {
super();
this.fbUserId = userDto.fbUserId;
this.fbFirstName = userDto.fbFirstName;
this.fbLastName = userDto.fbLastName;
this.gender = userDto.gender;
this.birthdate = userDto.birthdate;
this.aboutMe = userDto.aboutMe;
this.deviceToken = userDto.deviceToken;
this.refreshToken = userDto.refreshToken;
this.updatedAt = userDto.updatedAt;
this.createdAt = userDto.createdAt;
}
selectFields(fields) {
// --> what's your take?
}
toJson() {
return super.toJson();
}
}
Edit:
The service layer receives a dto from repository layer including all database fields. The ServiceLayerDto aims at filtering out fields that are not required by the web api (or should not be exposed as a security measure e.g. PK field, isDeleted, etc). So the result would I'm looking at the end of a service method for would look something like:
return new UserServiceDto(userDto)
.selectFields('fbUserId', 'fbFirstName', 'fbLastName', 'birthdate', 'aboutMe', 'updatedAt', 'createdAt')
.toJson();
The return value would be a plain json object that the web layer (controller) sends back to the http client.
If you are ok with spread operator, you may try following approach:
class UserServiceDto {
constructor() {
this.a = 1;
this.b = 2;
this.c = 3;
}
selectFields(...fields) {
const result = {};
fields.forEach(key => result[key] = this[key]);
return result;
}
}
new UserServiceDto().selectFields('a', 'c'); // {a: 1, c: 3}
Looking to super.toJson() call, I think that it would not work due to the result of my selectFields() call would not be an instance of UserServiceDto class. There are some possible ways from this point I see:
instantiate new UserServiceDto object inside selectFields() body, remove all fields that not listed in the ...fields array (javascript delete is okey) and return it;
play with UserServiceDto constructor params to save positive logic on selectFields(), and pass to constructor only that props that need to be set up; in this case instantiating a temporary object will not require properties removing;
change the signature of toJson method, or better add a new signature, which would allow to pass fields array and then put current selectFields logic inside toJson method (and remove selectFields method at all): new UserServiceDto().toJson('a', 'c')...
Purely for info, I ultimately changed my app architecture.
The repository returns a Dto to the service layer (dto being mapped directly from the sql queries).
The service builds a static View based on the Dto and returns it to the web layer (represented by a plain json object).
In my directory structure, I have:
- service
-- views
--- index.js
--- UserInfo.js
The view is a simple filter. E.g. UserInfoView:
exports.build = ({ fbUserId, fbFirstName, fbLastName, gender, birthdate, aboutMe, updatedAt, createdAt }) => {
return {
fbUserId,
fbFirstName,
fbLastName,
gender,
birthdate,
aboutMe,
updatedAt,
createdAt,
};
};
Using the view, e.g. UserInfoView in the service looks like this:
const Views = require('../service/views');
exports.findActiveByUserId = async (pUserId) => {
const userDto = await UserRepository.findActiveByUserId(pUserId);
if (!userDto) {
throw new ServiceError(Err.USER_NOT_FOUND, Err.USER_NOT_FOUND_MSG);
}
return Views.UserInfo.build(userDto.toJson());
};
I think that this is much more descriptive compared to my initial take on the problem. Also, it keeps the data objects plain (no additional methods required).
It is unfortunate that I can't require receiving an type (View) in the web layer, I might be able to solve that problem with Typescript later on.
I am trying to listen to a value changes with mobx computed expression, but I don't see any changes when I push a new value to the observed expression.
class List {
#observable values = [];
constructor() {
computed(() => this.values).observe(changes => {
console.log(changes);
})
}
add(item) {
this.values.push(Math.random());
}
}
const list = new List();
list.add();
Why doesn't it work?
Note that computed will only track data it actually accesses. The only data accessed in your computed is the changes, a pointer to an array. Pushing a new value to that array will not change the pointer.
Remember: computeds produce values, reactions & autoruns produce side effects.
Your computed never produces a new value, so never triggers the observer.
computed is used when you want to derive a new value from other observables. You could use observe instead:
Example (JSBin)
class List {
#observable values = [];
constructor() {
observe(this.values, (change) => {
if (change.added) {
console.log(`${change.added} got added to values`);
}
});
}
add(item) {
this.values.push(Math.random());
}
}
const list = new List();
setInterval(() => {
list.add();
}, 1000);
I have a model object representing "players" in DB. in it's implementation there is an array of players, which i would like to bind to from different VM's in my app. for example:
import {Players} from './models/players';
import {inject, BindingEngine} from 'aurelia-framework';
#inject(Players,BindingEngine)
export class App {
constructor(playersProvider,bindingEngine) {
this._playersProvider = playersProvider;
this._bindingEngine = bindingEngine;
this._subscription = this._bindingEngine.propertyObserver(this,this._playersCount)
.subscribe(this.objectValueChanged);
}
async activate() {
await this._playersProvider.initialize();
this._playersCount = this._playersProvider.players.length;
}
objectValueChanged(newVal,oldVal) {
console.log("new : " + newVal + ", old val : " + oldVal);
}
deactivate() {
this._subscription.dispose();
}
}
unfortunately, when a change is made to the players array (from other parts in the app) the change is not reflected in _playersCount property. e.g. - UI label bound to this property is not refreshed, and objectValueChanged never gets called.
U have the same issue in a different VM with a collectionObserver on the same array.
any help?
Have you tried to declare _playersCount in the constructor before subscribing to it?
Also the synthax does not seem correct, it should be according to this article:
import {BindingEngine, inject} from 'aurelia-framework';
#inject(BindingEngine)
class MyClass {
constructor(bindingEngine) {
this.bindingEngine = bindingEngine;
this.observeMe = 'myvalue'; // the property is first initialized in the constructor
let subscription = this.bindingEngine
.propertyObserver(this, 'observeMe') // <= you wrote this._bindingEngine.propertyObserver(this,this.observeMe)
.subscribe(this.objectValueChanged);
// Dispose of observer when you are done via: subscription.dispose();
}
objectValueChanged(newValue, oldValue) {
console.log(`observeMe value changed from: ${oldValue} to:${newValue}`);
}
}
The async keyword might affect the behaviour.
If it still does not work, you can use an event aggregator to broadcast the change.