Change template dynamically from view-model (Aurelia) - aurelia

Is it possible to change which html-template is being used dynamically from the view-model?
E.g. based on data downloaded from a server, I'd like to choose different templates (or some other logic in the view-model)
Update
Based on the answer below suggesting getViewStrategy, here's a complete sample:
export class MultiView {
gender : string
getViewStrategy() {
if(this.gender == 'boy')
return './multi-view-blue.html'
else
return './multi-view-pink.html'
}
// when view is made visible (e.g. by using the router)
activate() {
this.gender = Math.random()>0.5 ? "boy" : "girl"
}
}

If you want to do this on a single view model implement the getViewStrategy function.
export class MyView{
getViewStrategy(){
return 'my-other-view.html';
}
}

Refer to the documentation under App Configuration and Startup, titled Configuring the View Location Convention. Here's and excerpt:
To do this, during bootstrap, import the ViewLocator and replace its convertOriginToViewUrl method with your own implementation.
It includes a code example you may follow as well.
As an alternative, you could look into the aurelia-compiler library module.
NOTE: This library will be refactored and parts of it will be included into the core. In the meantime it can still be used but please be aware of this breaking change.
It contains a function called swapView() that looks like it may do what you want. An example of it being used is in the aurelia-dialog library module. Hopefully you can glean some useful information from that and find a way to make it work.

Write a view-model that takes data from server and binding variables of class.
export class MyClass{
constructor(){
this.red = false;
this.green = false;
this.yellow = false;
this.serverValue = "";
}
activate(){
return this.bindingFunction();
}
bindingFunction(){
if(this.serverValue == 'red') { this.red = true; }
else if(this.serverValue == 'green') { this.green = true; }
else this.yellow = true;
}
}
Write the view as a whole, with show.bind, and bind those with view-model.
<template>
<div show.bind='red'> /* your elements */ </div>
<div show.bind='green'> /* your elements */ </div>
<div show.bind='yellow'> /* your elements */ </div>
</template>

Related

ReactJS - app functions in different files

I'm trying to make React-based web game. I have an App component which holds pretty much all non-UX state. To avoid code duplication I also hold most functions in it and pass it down as prop to child components.
But now I'm starting to get cluttered by different functions, all in the App body. Is there any simple way to satisfactory structure this in different files? Should I already look into state management libraries?
Currently stuff looks like:
class App extends Component {
constructor(props) {
super(props);
this.state = gameInitialize();
this.modifyState = this.modifyState.bind(this);
this.moveUnit = this.moveUnit.bind(this);
this.progressMission = this.progressMission.bind(this);
this.timeJump = this.timeJump.bind(this);
this.competenceAfterTimeJump = this.competenceAfterTimeJump.bind(this);
this.save = this.save.bind(this);
this.load = this.load.bind(this);
}
componentDidMount() {
this.timerID = setInterval(this.modifyState, this.state.interval);
window.addEventListener('beforeunload', this.save);
this.load();
}
componentWillUnmount() {
clearInterval(this.timerID);
}
save() {
localStorage.setItem("gameSave", toJson(this.state));
}
load() {
let state = 0;
try {
state = fromJson(localStorage.getItem("gameSave"));
} catch (error) {
console.log(error);
return 0;
}
state.units.map(unit => {
delete unit.__parent;
delete unit.attributes.__parent
return 0;
});
state.missions.map(mission => delete mission.__parent);
this.setState(state);
}
modifyState() {
this.setState(this.state.units.map(this.progressMission));
this.setState(this.state);
}
progressMission(unit) {
const mission = unit.currentMission;
let increment = unit.attributes[mission.type].total() - mission.complexity;
if (increment < 0) increment = 0;
mission.progress += increment * this.state.interval / 1000 * unit.competence / 10;
if (mission.progress >= mission.difficulty) {
mission.progress = 0;
this.state.experience.get(mission.reward);
mission.completions += 1;
}
}
moveUnit(unit, mission) {
unit.currentMission = mission;
this.setState(this.state);
}
timeJump() {
const game = this.state;
while (game.units.length > 2) {
game.units.pop();
};
game.units.map(function (unit) {
Object.keys(unit.attributes).map((key) => { unit.attributes[key] = newAttribute() });
unit.currentMission = game.missions[0];
});
game.missions.map((mission) => {mission.progress = 0});
game.units[0].competence = this.competenceAfterTimeJump();
game.experience.current = 0;
this.setState(game);
}
competenceAfterTimeJump() {
return (10 + Math.sqrt(this.state.experience.total) / 10);
}
render() {
return (
<div className="App">
<header className="App-header">
<h1 className="title">Time-traveling Hero: eventually I'll save the world, or maybe not if I don't feel it</h1>
</header>
<SaveLoad game={this} />
<Prestige game={this} />
<MissionWrapper>
<MissionList missions={this.state.missions} game={this} />
</MissionWrapper>
<UnitWrapper>
<ExpWrapper>
<div>
Available Experience: {this.state.experience.current.toFixed(1)}
</div>
<div>
Total Experience: {this.state.experience.total.toFixed(1)}
</div>
</ExpWrapper>
<UnitList units={this.state.units} game={this} />
</UnitWrapper>
</div>
);
}
}
function gameInitialize() {
let game = { units: [], missions: [], currentUnit: undefined };
game.interval = 10;
game.missions = generateMissions(50);
game.experience = {
current: 0, total: 0,
get: function (amount) { this.current += amount; this.total += amount },
spend: function (amount) {
if (this.current >= amount) {
this.current -= amount;
return true;
}
else return false;
}
};
game.units.push({ name: "Hero", attributes: newAttributes(), competence: 10, currentMission: game.missions[0] });
game.units.push({ name: "Childhood Friend", attributes: newAttributes(), competence: 15, currentMission: game.missions[0] });
game.currentUnit = game.units[0];
game.missionsWithUnits = function () {
this.missions.map()
}
return game;
}
How should I proceed?
Yes, it's super easy to organize JS code! Use modules. Here's how to do it.
Export functions from a file
adders.js:
export function addTwo (number) {
return number + 2
}
Then use it:
This could be in a component file:
import { addTwo } from './path/to/adders.js'
console.log(addTwo(5)) // logs 7
You can organize this super well for a lot of things. If you have a group of related functions, use a module like this. here's the file structure:
mathStuff/
adders.js
index.js
You have all of your related files in the same folder and your functions exported from the individual files like above. Then set up index like this:
index.js:
import * as adders from './adders.js'
// Set up your object however you want.
const MathStuff = {
...adders
}
export default MathStuff
Then in any component you can do this:
import MathStuff from './path/to/mathStuff'
MathStuff.addTwo(7) // 9
For even more organization, you could set your index up to have functions like this:
index.js:
import * as adders from './adders.js'
import * as dividers from './dividers.js' // another math file with division functions or something
// Set up your object however you want.
const MathStuff = {
adders,
dividers
}
export default MathStuff
And use it like this:
import MathStuff from './path/to/mathStuff' // points to directory, NOT individual file
MathStuff.adders.addTwo(7) // 9
I would definitely suggest organizing code like this. One thing this improves is testability - it's very easy to test pure functions with no side effects.
I like to put my database code in one module and import it wherever to access all my database functions.
I like to put all of my business logic in different modules by category - for instance GameLogic or something like that.
This will also help you write more functional code. Currently, you have a lot of state modification within individual functions - you won't be able to do that in modules without binding individual functions to the this context of your react component. Instead, I would suggest passing all necessary parameters to the function and having it return a value. This moves business logic away, making it easier to manage state.
For instance, your progressMission function accesses this.state.interval. You can pass interval to the function itself.
One thing I'm noticing is that your code has a lot of dependency on each other - functions often have to access lots of things outside of itself, rather than being self-contained. It would probably help you a lot to try to refactor into a modular system, where functions are much more pure - only accessing what is passed to them, and returning values which get used. Using actual modules like above definitely helps do that - my code got better the more I did it. It helps you reason about your code better. Additionally, once/if you start implementing tests, you'll find that all of the tangled-ness of the code makes it hard to test - there are a lot of side effects.
Finally, redux and external state management probably won't help a ton in your case, but they might. Redux can help you achieve state that's easier to reason about, but it won't help you organize code better per se. I hope that helps!

Aurelia: Is there any option / decorator to restrict an attribute to a specific element

Hi I have created a custom element named as 'panel' and a custom attribute 'panel-type'. I want to restrict this 'panel-type' can be used only in 'panel' element. Is there any way to do that?
<panel value.bind='panel' panel-type='default'></panel>
--should work. But no other element can use 'panel-type'. like-
<some-other-tag panel-type='default'></some-other-tag>
--shouldn't work
Without seeing the code of your Custom Attribute - I can't tailor this to your specific needs - but it's easily possible;
In your Custom Attribute you have access to the the Element property - which is the physical element you've attached the attribute to. Therefore, you can simply use native JavaScript to get the elements tag name;
import {inject, LogManager} from "aurelia-framework";
#inject(Element)
export class panelTypeCustomAttribute {
allowedElememt = false;
constructor(Element) {
this.element = Element;
if(this.element.tagName == "PANEL") {
this.allowedElement = true;
} else {
LogManager.getLogger('testCustomAttribute').warn("The panel-type custom attribute can only be used on <panel /> elements");
}
}
foo() {
// Then you can check for the allowedElement flag in any of your methods;
if(this.allowedElement) {
// Do your thing
}
}
}

Aurelia UI Virtualization - Rebinding

I'm playing around with the cool Aurelia UI-Virtualization plugin (https://github.com/aurelia/ui-virtualization) to provide a user with a list of search results.
If they do a new search, I want to replace the current results with the new ones. I would think you just need to set the array to the new results, but that creates some weird behavior, kind of like the list is "remembering" it's old contents.
In my case, when you click on one of the search results, a separate panel shows details about that search result. But after a rebind, it shows info about the old result still.
Thanks!
Aaron
i managed to solve a similar problem using signals:
http://aurelia.io/hub.html#/doc/article/aurelia/binding/latest/binding-binding-behaviors/5
search.js:
import {inject} from 'aurelia-framework'
import {BindingSignaler} from 'aurelia-templating-resources'
export class Search
{
static inject() { return [BindingSignaler] }
constructor(signaler)
{
this.signaler = signaler
}
search()
{
// do your thing
this.searchresults = [ /* searchresults here */ ]
this.signaler.signal('update-results')
}
}
search.html
<template>
<div repeat.for="item in searchresults & signal:'update-results'">
${ item }
</div>
</template>

Create a new binding context for a custom attribute

What I want
<div amazingattr.bind="foo">
${$someValueFromAmazingattr}
</div>
Just like how this works:
<div repeat.for="bar of bars">
${$index}
</div>
Where I got stuck
import {customAttribute} from "aurelia-framework";
#customAttribute("amazingattr")
export class AmazingattrCustomAttribute {
bind(binding, overrideContext) {
this.binding = binding;
}
valueChanged(newValue) {
this.binding.$someValueFromAmazingattr = newValue;
}
}
While this works, the $someValueFromAmazingattr is shared outside the custom attribute's element, so this doesn't work:
<div amazingattr.bind="foo">
Foo: ${$someValueFromAmazingattr}
</div>
<div amazingattr.bind="bar">
Bar: ${$someValueFromAmazingattr}
</div>
Both of the "Foo:" and the "Bar:" show the same last modified value, so either foo or bar changes, both binding change to that value.
Why I need this?
I'm working on a value animator, so while I cannot write this (because value converters cannot work this way):
${foo | animate:500 | numberFormat: "0.0"}
I could write something like this:
<template value-animator="value:foo;duration:500">
${$animatedValue | numberFormat: "0.0"}
</template>
I imagine I need to instruct aurelia to create a new binding context for the custom attribute, but I cannot find a way to do this. I looked into the repeat.for's implementation but that is so complicated, that I could figure it out. (also differs in that is creates multiple views, which I don't need)
After many many hours of searching, I came accross aurelia's with custom element and sort of reverse engineered the solution.
Disclaimer: This works, but I don't know if this is the correct way to do it. I did test this solution within embedded views (if.bind), did include parent properties, wrote parent properties, all seem to work, however some other binding solution also seem to work.
import {
BoundViewFactory,
ViewSlot,
customAttribute,
templateController,
createOverrideContext,
inject
} from "aurelia-framework";
#customAttribute("amazingattr")
#templateController //This instructs aurelia to give us control over the template inside this element
#inject(BoundViewFactory, ViewSlot) //Get the viewFactory for the underlying view and our viewSlot
export class AmazingattrCustomAttribute {
constructor(boundViewFactory, viewSlot) {
this.boundViewFactory = boundViewFactory;
this.viewSlot = viewSlot;
}
bind(binding, overrideContext) {
const myBindingContext = {
$someValueFromAmazingattr: this.value //Initial value
};
this.overrideContext = createOverrideContext(myBindingContext, overrideContext);
//Create our view, bind it to our new binding context and add it back to the DOM by using the viewSlot.
this.view = this.boundViewFactory.create();
this.view.bind(this.overrideContext.bindingContext, overrideContext);
this.viewSlot.add(this.view);
}
unbind() {
this.view.unbind(); //Cleanup
}
valueChanged(newValue) {
//`this.overrideContext.bindingContext` is the `myBindingContext` created at bind().
this.overrideContext.bindingContext.$someValueFromAmazingattr = newValue;
}
}

Custom attribute in aurelia no working?

I am learning how Aurelia works and I am trying to get a simple custom attribute working. All it will do is change the color of a div text depending on some value changing.
I have a div which has:
high.bind="changeColor"
and in my attribute I have :
import {inject, customAttribute} from 'aurelia-framework';
#customAttribute('high')
#inject(Element)
export class High {
constructor(element) {
this.element = element;
}
valueChanged(newValue){
console.log(newValue);
if (newValue) {
this.element.classList.remove('highlight-yellow');
} else {
this.element.classList.add('highlight-blue');
}
}
In my view model I have :
import {high} from './highlightattribute'
export class Welcome{
heading = 'Welcome to the Aurelia Navigation App!';
firstName = 'John';
lastName = 'Doe';
get fullName(){
return `${this.firstName} ${this.lastName}`;
}
get changeColor(){
if (this.firstName == 'John'){
return false;
}
return true;
}
welcome(){
alert(`Welcome, ${this.fullName}!`);
}
}
When I change the firstname I do not see the valueChanged event being triggered in the high custom attribute class.
It looks like you are importing the high code in to your viewmodel rather than your view. Remove this line in your ViewModel:
import {high} from './highlightattribute'
Then and add this line to your View:
<require from="./highlightattribute"></require>
Next, in the highlightattribute.js file you are removing highlight-yellow and adding highlight-blue, so you will probably want to add and remove the same class. I did also notice that there is a missing parenthesis in your highlightattribute.js file you posted, but that was probably just missed while copying the code.
Let me know if this helps solve the problems. I have pushed a sample with your code to here: https://github.com/AshleyGrant/skeleton-navigation/tree/so-answer-20150416-01/src