vue.js - class component - Update view when RxJs Observable change data - vue.js

Disclaimer: I'm a beginner in using Vue.js, and being used to Angular I went the Class Component way. I know that not the proper Vue.js way, but this is a fun pet project, so I elected to do it in a unusual way in order to try new things.
I have a simple component, "MyForm", written using Typescript and the Class Component Decorator.
To simulate a service, I made a Typescript class "MyService", that contain methods simulating an API call using an RxJs Observable objects with a delay. The the service function update the data contained in another class "MyDataStore", which is then read by the component to update the view.
But when the observable returns and changes the Data in the Store, it does not update the displayed data (until the next clic on the button).
The component
<template>
<v-app>
<pre>
<v-btn #click="clickBouton()">Load</v-btn>
Form counter: {{counter}}
Service counter: {{service.counter}}
Store counter : {{store.counter}}
RxJs data : {{store.data}}
</pre>
</v-app>
</template>
<script lang="ts">
import { MyDataStore } from '#/services/MyDataStore';
import { MyService } from '#/services/MyService';
import Vue from 'vue'
import Component from 'vue-class-component';
#Component
export default class MyForm extends Vue {
public counter = 0;
public store = MyDataStore;
public service = MyService;
clickBouton(){
this.counter++;
MyService.Increment()
MyService.getData();
}
}
</script>
The service
import { of, from, concatMap, delay } from 'rxjs';
import { MyDataStore } from './MyDataStore';
export class MyService {
public static counter = 0
// Update the data store with a new value every 10s
static getData(){
from(["A", "B", "C", "D"]).pipe(concatMap(item => of(item).pipe(delay(10000)))).subscribe((res: any) => {
MyDataStore.data = res;
});
}
static Increment(){
MyDataStore.counter++;
MyService.counter++
}
}
The "DataStore"
export class MyDataStore {
static data: string;
static counter = 0;
}
Any help or tutorial are welcome.

Hey In the end you get a observable. You need to subscribe a value of your component to it and descubscribe it if you destroy your component. If you are using Vue 2 you can use async pipes/filter in order to automate this process.
I found this tutorial:
https://medium.com/#p.woltschkow/a-better-practice-to-implement-http-client-in-vue-with-rxjs-c59f93bfa439

Related

Spartacus Multisite Site Specific Configuration

We'd like to store some site-specific config on the frontend in Spartacus.
For example: Each site (of a multisite setup) has a different Google API Key.
Right now, I've built a CONFIG_INITIALIZER factory, like the following. But with the fake scoping and all, it does not seem the correct way to do this.
import { Inject, Injectable } from '#angular/core';
import { ConfigInitializer, ConfigInitializerService, deepMerge } from '#spartacus/core';
import { StoreFinderConfig } from '#spartacus/storefinder/core';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { MultiSiteConfigChunk } from './multisiteconfig-tokens';
#Injectable({ providedIn: 'root' })
export class MultisiteConfigInitializer implements ConfigInitializer {
// Fake scoping :(
readonly scopes = ['all'];
readonly configFactory = () => this.resolveConfig().toPromise();
constructor(
protected configInit: ConfigInitializerService, #Inject(MultiSiteConfigChunk) private multiSiteConfig: Array<any>) {
}
protected resolveConfig(): Observable<StoreFinderConfig> {
return this.configInit.getStable('context.baseSite').pipe(
map((config) => {
const mergedConfig = deepMerge(...this.multiSiteConfig);
return mergedConfig[config.context.baseSite];
})
);
}
}
What would be the recommended way to get be able to do this?
You can check this doc first: https://sap.github.io/spartacus-docs/automatic-context-configuration/#adding-a-custom-context
If you are using automatic context configuration, you also can try this:
extends SiteContextConfigInitializer (inject your multiSiteConfig in the child class); replace SiteContextConfigInitializer using the child in providers.
replace getConfig function in child class to add the Google API Key to site-context.

Vuex ORM models dependency cycle

In the store, I have two related models: Company and User
User
import { Model } from '#vuex-orm/core';
import { Company } from './models';
export class User extends Model {
static entity = 'users';
static fields() {
return {
company: this.belongsTo(Company, 'company_id'),
};
}
}
export default User;
Company
import { Model } from '#vuex-orm/core';
import { User } from './models';
export class Company extends Model {
// This is the name used as module name of the Vuex Store.
static entity = 'companies';
static fields() {
return {
account_manager: this.belongsTo(User, 'account_manager_id'),
};
}
}
export default Company;
To avoid dependency cycle, I closely followed the solution from https://vuex-orm.org/guide/model/single-table-inheritance.html#solution-how-to-break-cycles
and import Company and User into models.js
Models
export * from './company';
export * from './user';
Yet I still get the dependency cycle error from the linter.
I run out of ideas.
Code sample: https://github.com/mareksmakosz/vuex-orm-dependency-cycle
This is just ESLint enforcing the rule, you can't avoid it if you're using airbnb. Consider eslint:recommended if it's truly a pain.
Alternatively, if you want to keep airbnb and your models separated, I suggest dropping the imports and define your relations using entity as a string.
this.belongsTo('companies', 'company_id')
this.belongsTo('users', 'account_manager_id')

Angular 5 BehaviorSubject data not update UI

I use to change dynamic child component in body and keep static header, bottom and menu.
My problem: When use BehaviorSubject as shared-data between components, then UI (*ngFor) not be updated event shared-data transferred well. I am using Angular 5.2.0, RxJs 5.5.6
My app has flow:
user click search button on Layout-top.component.ts -> fetch data from Backend server by Home.service.ts-> set data in BehaviorSubject object.
On Home.component.ts constructor always subscribe shared-data from Home.service.ts -> change data of Home.component.ts -> display them.
1. App.compoenet.ts
#Component({
selector: 'xxx',
template:
`
<gotop position="200"></gotop>
<layout-top></layout-top>
<router-outlet></router-outlet>
<layout-bottom></layout-bottom>
`
})
export class AppbComponent implements OnInit, AfterViewInit{
public ngAfterViewInit(): void {
this.spinner.hide();
}
message:string;
constructor(private spinner:Spinner){
}
public ngOnInit(){
this.spinner.show();
}
}
Layout-top.component.ts
public doSearch(){
let filter = {
xx:'XXX'
};
this.homeService.setData(filter);
}
3.Home.service.ts
#Injectable()
export class HomeService extends BaseService{
public data =new BehaviorSubject<DataType>(<DataType>{});
public eventFilter: EventEmitter<{}> = new EventEmitter();
public constructor(private http: HttpClient,
private _const: Const,
private util:Util,
private appref: ApplicationRef) {
super(_const, util);
}
public listProduct(filter):Observable<any>{
const url = url to my backend api
let headers:HttpHeaders = this.util.header(this._const, null, 'application/json');
return this.http.post(
url,
filter,
{headers})
.map(res => {
return res;
});
}
public getData():Observable<DataType>{
return this.data.asObservable();
}
public setData(filter:any):void {
const listProduct$ = this.listProduct(filter);
listProduct$.subscribe(res => {
this.data.next({res:res, filter:filter});
});
}
public cleanData() {
this.data.next(null);
}
}
layout-top.html
5.home.html
<div class="product-item"
*ngFor="let item of listProducts">
<!--display some thing here-->
</div>
6.home.component.ts
constructor(private service: HomeService,
private cdRef:ChangeDetectorRef,
private zone:NgZone,private appref: ApplicationRef ){
this.subsListProduct = this.service.getData().subscribe(obj=>{
this.zone.run(()=>{
$("#in-blur").css("display", "block");
if(!obj){
return;
}
const res = obj.res;
const filter = obj.filter;
if(res && filter){
this.listProducts = res.list;
this.cdRef.detectChanges();
}
});
setTimeout(()=>{
$("#in-blur").css("display", "none");
}, 1000);//for test loading spinner. will be remove in product
});
}
"this.listProducts = res.list;" work fine, ther listProducts be updated, but UI is not any change.
Many people advised use zone.run() or ChangeDetectorRef.detectChanges() but not work in my app. Plz support me.
The best way to map observable data to views is to use the async pipe - https://angular.io/api/common/AsyncPipe - this way all the change management and unsubscribing form the observable is handled for you. So in your example:
The view:
<div class="product-item"
*ngFor="let item of (listProducts | async)?.list">
<!--display some thing here-->
</div>
The ? before .list makes the property optional, so nothing will break if list is is null or undefined
The home component:
constructor(private service: HomeService,
private appref: ApplicationRef ){
this.listProducts = this.service.getData();
}
If you need to manipulate any of the data from the observable before displaying in the view do that in a .map, eg.
constructor(private service: HomeService,
private appref: ApplicationRef ){
this.subsListProduct = this.service.getData()
.map(obj => {
if (obj.list && obj.filter) {
return obj;
} else {
return null;
}
});
}
You should also consider dropping jquery for doing your css changes and use ngClass in your view to do this instead - https://angular.io/api/common/NgClass

How to use a function from injected class in Aurelia?

How to use a function from injected class in Aurelia? In my case, I'm calling a login() function (on the login page) and want to get the testMessage printed from Test Class. Unfortunately, I'm getting the following error in Chrome: "Uncaught TypeError: Cannot read property 'getTestMessage' of undefined".
test.js
export class Test {
constructor() {
this.testMessage = "test";
}
getTestMessage() {
return this.testMessage;
}
}
login.js
import {inject} from 'aurelia-framework';
import {Test} from 'test';
#inject(Test)
export class LogIn {
constructor(test) {
this.test = test;
}
login(){
console.log("login");
this.test.getTestMessage();
}
}
There is nothing wrong with your code. I copied it over to a test application and it worked. The issue most likely lies in the part you didn't reveal in your question: how you are calling the login method.
Login
In my test example, the view-model injecting test and subsequent HTML view is calling the login method using click.delegate. How are you calling the method?

Is it possible to make a function or class available to all views in Aurelia?

The use case is as follows: We would like to have elements hidden or shown, based on the user's permissions.
The ideal way would be something like this:
<div if.bind="foo != bar && hasPermission('SOME_PERMISSION')"></div>
hasPermission() would in that case be a function that was automatically injected into all viewmodels.
Is that possible? I know we could use base classes for this, but we'd like to avoid that to stay as flexible as possible.
If you're willing to pay the price of a global function (global as in window), import it in your app-bootstrap file, like so:
has-permission.js
export function hasPermission(permission) {
return permission.id in user.permissions; // for example...
}
main.js
import 'has-permission';
export function configure(aurelia) {
// some bootstrapping code...
}
If the service you want to publish globally is a view, you can sidestep exposing it on the window and tell Aurelia's DI to make it available everywhere so you won't have to declare it in every dependent client.
To do so, pass its module ID in the FrameworkConfiguration#globalResources() configuration function:
export function configure(aurelia) {
aurelia.use
.standardConfiguration()
.globalResources('my-kick-ass-view', 'my-awesome-converter');
aurelia.start().then(a => a.setRoot());
}
If you have a service which deals with user permission, it can be injected in all your view-models.
export class UserPermissionService
{
hasPermission(user, permission)
{
return false;
}
}
#inject(UserPermissionService)
export class Users {
userPermissionService;
constructor(userPermissionService) {
this.userPermissionService = userPermissionService;
...
}
hasPermission(user, p)
{
return this.userPermissionService.hasPermission(user, p);
}
}
If you still don't like this, other options are:
a value converter http://aurelia.io/docs.html#/aurelia/binding/1.0.0-beta.1.2.1/doc/article/binding-value-converters
a custom attribute (similar to if it will hide the element)
http://www.foursails.co/blog/custom-attributes-part-1/
depending on what you need, both can use the UserPermissionService singleton from above
Add a .js file in your folder, with export function
ex: utility.js
export function hasPermission(permission) {
return true/false;
};
import the function in view-model
import {hasPermission} from 'utility';
export class MyClass{
constructor(){
this.hasPermission = hasPermission;
}
}
view.html
<div if.bind="foo != bar && hasPermission('SOME_PERMISSION')"></div>