Pass global Object/Model to custom element in Aurelia - aurelia

referring to the following post StackOverflow Question I have a quite different scenario where I want to know if Aurelia has a solution for.
Scenario:
I have a user model:
export class User{
#bindable name: string;
#bindable address: Address
As you can see, "Address" is a sub-model.
I have a main view-model "registration". In this view model I have a model "user":
export class RegistrationView{
#bindable user: User
public attached(){
this.user = userService.fetchUserFromApi();
}
In addition to that I have a custom-element "user-address" where I have a "user-address"-model (because I want to have dedicated encapsulated custom-elements).
export class userAddress{
#bindable userAddress: Address
Now I want to request the user model only once from the API and send the user address it to the custom-element:
<template>
<require from="user-address"></require>
<user-address user.address.bind="${dj.address}"></user-address>
Finally I would (to have dedicated encapsulated custom-elements that I can use everywhere) check in attached method if the user is already load and if not then the custom-element would load all needed data:
export class userAddress{
#bindable userId: string
#bindable address: Address
public attached(){
if(!(typeof this.address === "undefined")){
this.address = this.addressAPIService.getAddressByUserId(id)
}
}
Problem 1: I know, that the mentioned template dj.address.bind doesn't work. But now my question is, how can I handle that situation?
Problem 2: How do I assure, that the user object is only requested once?
Does my concept makes sense and does it is the idea of Aurelia?

If I understand your problem correctly, you simply need some form of client-side persistence.
If you need this persistence even after the user closed the browser, you'll want to use either localStorage or some encapsulation thereof. There are many good plugins available such as localForage, LokiJS and a recently developed (still in beta) aurelia plugin aurelia-store
You probably want to encapsulate the retrieval of your user in a UserService of some sort. This is nothing specific to Aurelia, just generally how you want to do this in most types of applications.
Example
So in your viewmodel you might have something like this (skipping some of the implementation details such as checking the params, configuring the router etc for brevity):
#autoinject()
export class UserViewModel {
public user: User;
constructor(private userService: UserService){}
// happens before bind or attached, so your child views will always have the user in time
public async activate(params: any): Promise<void> {
this.user = await this.userService.getUserById(params.id);
}
}
And in your userservice:
// singleton will ensure this service lives as long as the app lives
#singleton()
export class UserService {
// create a simple cache object to store users
private cache: any = Object.create(null);
constructor(private client: HttpClient) {}
public async getUserById(id: number): Promise<User> {
let user = this.cache[id];
if (user === undefined) {
// immediately store the user in cache
user = this.cache[id] = await this.client.fetch(...);
}
return user;
}
}
Let your view model just be dumb and call the UserService whenever it needs to load a user, and let your service be clever and only fetch it from the API when it's not already cached.
I'd also like to point out that attached() is not when you want to be grabbing data. attached() is when you do DOM stuff (add/remove elements, style, other cosmetic things). bind() is best restricted to grabbing/manipulating data you already have on the client.
So when to fetch data?
In your routed view models during the routing lifecycle. That'll be configureRouter, canActivate, activate, canDeactivate and deactivate. These will resolve recursively before any of the DOM gets involved.
Not in your custom elements. Or you'll soon find yourself in maintenance hell with notification mechanisms and extra bindings just so components can let eachother know "it's safe to render now because I have my data".
If your custom elements can assume tehy have their data once bind() occured, everything becomes a lot simpler to manage.
And what about API calls invoked by users?
More often than you think, you can let an action be a route instead of a direct method. You can infinitely nest router-views and they really don't need to be pages, they can be as granular as you like.
It adds a lot of accessibility when little sub-views can be directly accessed via specific routes. It gives you extra hooks to deal with authorization, warnings for unsaved changes and the sorts, it gives the user back/forward navigation, etc.
For all other cases:
Call a service from an event-triggered method like you normally would during activate(), except whereas normally the router defers page loading until the data is there, now you have to do it yourself for that element.
The easiest way is by using if.bind="someEntityThatCanBeUndefined". The element will only render when that object has a value. And it doesnt need to deal with the infrastructure of fetching data.

Related

vuex-persistedstate not saving class methods

I'd like to preference this by saying my backgrounds in in C# so I like declaring methods within my classes. I've created a user class that contains properties and methods and I've added this to my vuex-persistedstate. One of the methods is a logout() method which clears out the properties. When I tried to invoke this method I got the following error:
TypeError: this.$data.user.logout is not a function
I then reviewed local storage and noted the user did not have reference to the class method. So I went ahead and copied the logic from the method into my vue component and it worked so I'm assuming the issue is vuex-persistedstate does not save references to methods which is why the method call did not work.
I'd like to declare the logout method in one location rather than spreading it out across vue components, what is the best practice for accomplishing this? Is it possible to do this in the class declaration or do I need a user helper file?
Sure Berco! My code is also up on GitHub so you can review it there too, but basically it seems to me that vuex does not store methods. The first file you should review is my user.js file:
https://github.com/Joseph-Anthony-King/SudokuCollective/blob/master/SudokuCollective.WebApi/client/src/models/user.js
In this file I have a method called shallow clone which takes the info received from the API and assigns it to the user:
shallowClone(data) {
if (data !== undefined) {
this.id = data.id;
this.userName = data.userName;
this.firstName = data.firstName;
this.lastName = data.lastName;
this.nickName = data.nickName;
this.fullName = data.fullName;
this.email = data.email;
this.isActive = data.isActive;
this.isAdmin = data.isAdmin
this.isSuperUser = data.isSuperUser;
this.dateCreated = data.dateCreated;
this.dateUpdated = data.dateUpdated;
this.isLoggedIn = data.isLoggedIn;
}
}
You of course don't need to abstract this away but I've found it makes the code easier to maintain.
Then in the mounted() lifecycle hook I assign the user received from the API to the component user via the shallowClone method. Please bear in mind I've done additional work on this project and the login form is now it's own component which receives the user as a prop from the app:
https://github.com/Joseph-Anthony-King/SudokuCollective/blob/master/SudokuCollective.WebApi/client/src/components/LoginForm.vue
mounted() {
let self = this;
window.addEventListener("keyup", function (event) {
if (event.keyCode === 13) {
self.authenticate();
}
});
this.$data.user = new User();
this.$data.user.shallowClone(this.$props.userForAuthentication);
},
The full code can be reviewed here:
https://github.com/Joseph-Anthony-King/SudokuCollective
I found a solution... I'm working on improving it. Basically I use the values pulled from localstorage into vuex to create a new user object in the vue component that has reference to the methods located in my user class declaration. I recalled recommendations that we should create clones of objects pulled from vuex for use within the vue component. I'm still refining the code but that's basic idea.

How to add my custom page resolver instead of Spartacus page resolver?

I'm adding my resolver (which extended from PageMetaResolver) into providers in my own home.module. However, my method 'resolve' is not called. Have you got any ideas?
#Injectable({
providedIn: 'root'
})
export class HomePageMetaResolver extends PageMetaResolver implements PageDescriptionResolver {
constructor(
protected routingService: RoutingService,
protected translationService: TranslationService,
protected cms: CmsService
) {
super();
this.pageType = PageType.CONTENT_PAGE;
}
resolve(): Observable<PageMeta> {
console.log('RESOLVE')
return this.cms.getCurrentPage().pipe(
switchMap(page =>
combineLatest([
this.resolveDescription()
])
),
map(([description]) => ({ description }))
);
}
resolveDescription(): Observable<string> {
return new Observable(sub => {
sub.next('test description');
});
}
}
TL;DR
Please specify not only this.pageType, but also this.pageTemplate for your PageMetaResolver.
Detailed explanation
The most specific Page Meta Resolver wins
Guessing from the name of your meta resolver, you want to provide custom meta only for your Home page. Please note that the most specific meta resolver wins thanks to the simple scoring algorithm, which takes into consideration 2 factors: page type and page template (see source https://github.com/SAP/cloud-commerce-spartacus-storefront/blob/develop/projects/core/src/cms/page/page-meta.resolver.ts#L11).
Currently your resolver HomePageMetaResolver has the same specificity as the generic one so your resolve method is not called.
You need to specify more your meta resolver by defining its property this.pageTemplate = '<name-of-page-template-used-only-at-homepage>'. Then your HomePageMetaResolver will get higher score than a default ContentPageMetaResolver whenever you visit a homepage.
Custom scoring algorithms
For custom scoring algorithms (which could take pageId into account, for instance), you can overwrite the method getScore of your PageMetaResolvers. You can even extend the method PageMetaService.getMetaResolver to redefine all rules of chosing the right page meta resolver. But for your case the standard solution (described above) should suffice.

Aurelia - ValidationController always returns true

I've made a wizard with aurelia and I want to check the input values when the user hits the next button.
#autoinject()
export class settingsWizard {
steps: Step[];
activeStep: Step;
valController: ValidationController;
constructor(private i18n: I18N, private http: HttpClient, public wizard: Wizard, private log: Logger, private router: Router, private controllerFactory: ValidationControllerFactory) {
this.steps = [
new Step(1, this.i18n.tr("route1"), PLATFORM.moduleName('./steps/step1')),
new Step(2, this.i18n.tr("route2"), PLATFORM.moduleName('./steps/step2')),
new Step(3, this.i18n.tr("route3"), PLATFORM.moduleName('./steps/step3')),
new Step(4, this.i18n.tr("route4"), PLATFORM.moduleName('./steps/step4')),
];
ValidationRules
.ensure('availableTotalTime').required().on(this.wizard.plannedTime);
//.ensureObject()
//.satisfies((x: IQualityRate) => x.numberOfProducedQuantity > x.rejects)
//.withMessage(this.i18n.tr('quality-rate.rejectsHigherThanProducedQuantity'))
//.on(wizard.qualityRate);
this.valController = controllerFactory.createForCurrentScope();
this.valController.validateTrigger = validateTrigger.manual;
}
here's the nextButton event:
nextStep() {
this.valController.validate().then(result => {
if (result.valid) {
if (this.activeStep.id !== this.steps.length) {
this.activeStep = this.steps[this.activeStep.id];
}
}
});
}
My aurelia ValidationController always returns result.valid = true even though availableTotalTime is undefined. What am I doing wrong?
You might be assuming that the ValidationRules api automatically causes objects to be validated, but it only stores the rules on the object's prototype.
The ValidationController only validates objects which are either explicitly (1) or implicitly (2) registered, or directly provided (3) to it.
1. Register "implicitly"
Any property in a view that has the & validate binding behavior on it will be validated whenever you call controller.validate() in the same viewmodel.
2. Register explicitly
Likewise, controller.addObject(this.wizard.plannedTime) will cause that object (and all its properties) to be validated on subsequent calls to .validate()
3. Provide a ValidateInstruction
this.valController.validate({
object: this.wizard.plannedTime,
// optional: propertyName: ...,
// optional: rules: ...
});
This gives you the highest degree of control. It will only validate the object you pass in, and nothing else. Also it won't register the object, so subsequent calls to .validate() will not automatically validate it again.
You'll probably either want to go with option 2 or 3, depending on other concerns.
Ended up here as I was getting the same problem. Fred Kleuver's answer is correct, but only when using .on to register your view model (or in this case, the object with the property to be validated) when creating your rules.
If you are like me and you're explicitly passing in the validation rules into the binding behavior, like & validate:myRules (this happens to be how most of the examples I've seen online doing it) you will need to follow the Entity Validation portion of the documentation.That way you can explicitly pass your validation rules into your validationController.addObject call in the same way you would pass your rules to your binding behavior in your view.
this.validationRules = ValidationRules
.ensure('availableTotalTime')
.required()
.rules;
this.validationController.addObject(this.wizard.plannedTime, this.validationRules);

If method returns data then load in module(view) - Aurelia

Forgive me for my ignorance but I've just started out with Aurelia/ES6 and a lot baffles me at the moment. I'm completely new to client side frameworks, so hopefully what I'm trying to achieve is possible within the framework.
So as the title indicates I'm fetching data within a class:
import {inject} from "aurelia-framework";
import {HttpClient} from "aurelia-http-client";
let baseUrl = "/FormDesigner/";
#inject(HttpClient)
export class FormData{
constructor(httpClient)
{
this.http = httpClient;
}
GetFormById(formId)
{
return this.http.get(`${baseUrl}/GetFormById/${formId}`)
.then(resp => resp.content);
};
}
Now I can see/receive the data which is great but after digging into the docs I cannot seem to figure out:
Load a separate related module/view by Id into the main view (app.html)
If no data, error and no Id passed then redirect to no-form view
Scenario:
User A navigates to "FormDesigner/#/form/3E7689F1-64F8-A5DA0099D992" at that point "A" lands on the form page, now if successful and data has been returned pass the formId into a different method elsewhere and then load in a module/view - Pages, possibly using <compose></compose>
This is probably really simple but the documentation (in my opinion) seems rather limited to someone that's new.
Really appreciate any guidance/high level concepts, as always, always much appreciated!
Regards,
Sounds like you might want to just partake in the routing lifecycle
If you are navigating to a module you can create an activate method on the view model which will be called when routing starts.
In this method you can return a promise (while you fetch data) and redirect if the fetch fails
In fact if the promise is rejected, the routing will be cancelled
If successful you can use whatever method you need to load in your module (assuming it can't just be part of the module that is being loaded since routing won't be cancelled)
Something like
activate(args, config) {
this.http.get(URL).then(resp => {
if (resp.isOk) {
// Do stuff
} else {
// Redirect
}
});
}

How to keep your MVC controllers DRY for Edit->Save->ValidateFail

I've got a Manage User event that takes an an optional userID and displays a user edit screen. There is a manageUserViewModel to go with this screen.
My Manage page has some dependencies - eg, PageTitle, what method to submit to, etc.
If I validate-fail, I need to show the manage screen again, but this time, using the view-model that was passed into the same method.
Supplying these dependencies in the fail scenario isn't very DRY.
How do I step repeating the dependencies? I tried putting them into a separate method, but that does not feel right.
public ActionResult Manage(Guid? UserID)
{
User user = this._UserLogic.GetUser(UserID);
ViewBag.Title = "User List";
ViewBag.OnSubmit = "Save";
ManageUserViewModel uvm = Mapper.Map<User, ManageUserViewModel>(user);
return View("Manage", uvm);
}
[AcceptVerbs("POST")]
public ActionResult Save(ManageUserViewModel uvm)
{
User user = this._UserLogic.GetUser(uvm.UserID);
if (!ModelState.IsValid)
// This is not very DRY!!!
ViewBag.Title = "Manage User";
ViewBag.OnSubmit = "Save";
return View("Manage", uvm);
}
Mapper.Map<ManageUserViewModel, User>(uvm, user );
this._UserLogic.SaveUser(user);
return RedirectToAction("Manage", new { UserID = user.ID });
}
I think you misunderstand DRY. DRY does not mean "NEVER repeat yourself", it means that you should not repeat yourself when it makes sense not to.
Different views have different requirements, and creating a complex structure just to avoid repeating yourself violates other best practices, like KISS, and SRP.
SOLID is interesting because Single Responsibility Principle is often at odds with Don't Repeat Yourself, and you have to come up with a balance. In most cases, DRY loses because SRP is far more important.
It looks to me like you have code here that is handling multiple responsibilities just so you can avoid writing similar code more than once. I disagree with doing that, because each view has different responsibilities and different requirements.
I would suggest just creating separate controller actions, views, and models for each action, particularly if the validation requirements are different for them. There may be a few things you can do (like using Partial Views or Editor Templates) to reduce repetition, but in general don't add lots of complexity just to avoid repetition.
You could add the 'Manager User' Title and 'Save' OnSubmit strings as properties of on the ManageUserViewModel. This means that you would not have to add them to the ViewBag each time you called Save.
You could also make a ManageUserService which could be responsible for the AutoMapper mappings and saving the user.
You code would then look like this:
public ActionResult Manage(Guid? UserID)
{
var uvm = _userService.GetById(UserId);
return View("Manage", uvm);
}
[AcceptVerbs("POST")]
public ActionResult Save(ManageUserViewModel uvm)
{
if (!ModelState.IsValid)
{
return View("Save", uvm);
}
_userService.Save(uvm);
return RedirectToAction("Manage", new { UserID = uvm.ID });
}
Just put the CRUD logic and AutoMapping functionality in the a class called UserService, and instance of which can be injected using Inversion of Control into your controller.
If you don't want to hard-code your string values into the view model itself, then you could add the values to an ApplicationResources file and reference those from the view model.
You will have to find some way to preserve this information between requests, which either means passing it back and forth between the client and server or saving it on the server. Saving it on the server means something like session but this feels a little heavy to me. You could add it to your ViewModel as #Ryan Spears suggested. To me that feels a little wrong, polluting the ViewModel with something that might be considered metadata. But that is just an opinion and I am not discrediting his answer because it is valid. Another possibility would be to just add the extra fields to the parameter list of the action method itself and use hidden fields.
[AcceptVerbs("POST")]
public ActionResult Save(ManageUserViewModel uvm, string title, string onSubmit)
{
...
}
In the form add:
<input type="hidden" name="title" value="#ViewBag.Title" />
<input type="hidden" name="onSubmit" value="#ViewBag.OnSubmit" />
This is essentially the same concept and solution as adding them to the ViewModel except in this situation they are not actually part of the ViewModel.
You can use RedirectToAction() and then export and import your tempdata (to maintain the ModelState) if you're worried about the 3 lines.
Personally I'd find it a lot more readable if you kept the logic in the POST version of the method, as you're performing something slightly different from the GET method, therefore not really repeating yourself. You could you probably keep the two ViewBag variables you have inside the View, and then there's no repetition at all.
As a side note: [HttpPost] now supersedes [AcceptVerbs]
We have come up with another solution that I thought I would share.
This based on the view-model containing info on what actions it can do, but we feel the controller should be specifying these (ie, controlling what actions different links route to) these because we have cases where the view-models are reused across actions. EG, the case where when you edit you can edit a template or an instance of something - the UI is the same, the only difference is the actions you post to/cancel from.
We abstracted away the part of the view-model that contains the data bound properties and the view model that contains other things we need for the view to render. We call the property-only object a DTO - it's not a true dto because it contains validation attributes.
We figure that we might be able to re-use these DTO's in the future for ajax or even XML requests - it, can keep validation DRY.
Anyway - here is an example of the code, we are happy with it (for now) and hope it helps others.
[HttpGet]
[ValidateInput(false)]
public virtual ActionResult ManageUser(ManageUserDTO dto, bool PopulateFromObject = true)
{
User user = this._UserLogic.GetUser(dto.UserID);
if (PopulateFromObject)
Mapper.Map<User, ManageUserDTO>(user, dto);
ManageUserViewModel vm = new ManageUserViewModel()
{
DTO = dto,
PageTitle = Captions.GetCaption("pageTitle_EditUser"),
OnSubmit = GetSubmitEventData(this.ControllerName, "SaveUser"),
OnCancel = GetCancelEventData(this.ControllerName, "ListUsers"),
};
return View("ManageUser", vm);
}
[HttpPost]
public virtual ActionResult SaveUser(ManageUserViewModel vm)
{
User user = this._UserLogic.GetUser(vm.DTO.UserID);
if (!ModelState.IsValid)
{
return ManageUser(vm.DTO, false);
}
Mapper.Map<ManageUserDTO, User>(vm.DTO, user);
this._UserLogic.SaveUser(user);
TempData.AddSuccess(Captions.GetCaption("message_UserSavedSuccessfuly"));
return RedirectToAction("ManageUser", new { UserID = user.ID });
}
The model-binder will set any URI variables into the dto in the get action. My logic layer will return a new User object if a call to getUserByID(null) is made.