Aurelia iterate over map where keys are strings - aurelia

I'm having trouble getting Aurelia to iterate over a map where the keys are strings (UUIDs).
Here is an example of the data I'm getting from an API running somewhere else:
my_data = {
"5EI22GER7NE2XLDCPXPT5I2ABE": {
"my_property": "a value"
},
"XWBFODLN6FHGXN3TWF22RBDA7A": {
"my_property": "another value"
}
}
And I'm trying to use something like this:
<template>
<div class="my_class">
<ul class="list-group">
<li repeat.for="[key, value] of my_data" class="list-group-item">
<span>${key} - ${value.my_property}</span>
</li>
</ul>
</div>
</template>
But Aurelia is telling me that Value for 'my_data' is non-repeatable.
I've found various answer by googling, but they have not been clearly explained or incomplete. Either I'm googling wrong or a good SO question and answer is needed.

As another resource to the one supplied by ry8806, I also use a Value Converter:
export class KeysValueConverter {
toView(obj) {
if (obj !== null && typeof obj === 'object') {
return Reflect.ownKeys(obj).filter(x => x !== '__observers__');
} else {
return null;
}
}
}
It can easily be used to do what you're attempting, like this:
<template>
<div class="my_class">
<ul class="list-group">
<li repeat.for="key of my_data | keys" class="list-group-item">
<span>${key} - ${my_data[key]}</span>
</li>
</ul>
</div>
</template>

The easiest method would be to convert this into an array yourself (in the ViewModel code)
Or you could use a ValueConverter inside repeat.for as described in this article Iterating Objects
The code...
// A ValueConverter for iterating an Object's properties inside of a repeat.for in Aurelia
export class ObjectKeysValueConverter {
toView(obj) {
// Create a temporary array to populate with object keys
let temp = [];
// A basic for..in loop to get object properties
// https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Statements/for...in
for (let prop in obj) {
if (obj.hasOwnProperty(prop)) {
temp.push(obj[prop]);
}
}
return temp;
}
}
/**
* Usage
* Shows how to use the custom ValueConverter to iterate an objects properties
* aka its keys.
*
* <require from="ObjectKeys"></require>
* <li repeat.for="prop of myVmObject | objectKeys">${prop}</li>
*/
OR, you could use the Aurelia Repeat Strategies provided by an Aurelia Core Team member
You'd have to import the plugin into your app.
Then you'd use it using the pipe syntax in your repeat.for....like so....
<div repeat.for="[key, value] of data | iterable">
${key} ${value.my_property}
</div>

Related

How to access the validator results in an TYPO3 Extbase action?

I have an Extbase module with a new/create-action. The model has the #validate annotation in it.
So far so good, everything's working.
But: I don't like how the form-errors are presented in the view. I'd like to add a CSS class like error in the view to the fields that are not correctly filled in.
But the only way to access the errors in the form seems to be through the <f:form.validationResults>-Viewhelper.
When I try to debug the results with <f:debug>{validationResults}</f:debug> I get a NULL value.
How do I access an error for a single field?
Actually, I'd prefer to access the errors in the controller, so I could pass an array to the view with the fields that contain an error.
I'm using Fluid and TYPO3 9.5
Such Fluid template should work out-of-the-box:
<f:form.validationResults>
<f:if condition="{validationResults.flattenedErrors}">
<ul>
<f:for each="{validationResults.flattenedErrors}" key="propertyPath" as="errors">
<li>
{propertyPath}: <ul>
<f:for each="{errors}" as="error">
<li>{error}</li>
</f:for>
</ul>
</li>
</f:for></ul>
</f:if>
</f:form.validationResults>
Another approach,
You can also write own ViewHelper to display error messages as you want in your own form HTML markup:
<?php
namespace VENDOR\Yourext\ViewHelpers;
use TYPO3\CMS\Extbase\Utility\DebuggerUtility;
use TYPO3Fluid\Fluid\Core\ViewHelper\AbstractViewHelper;
/**
* #author Marcus Biesioroff biesior#gmail.com>
*
* ViewHelper for displaying custom-designed errors
*
* Usage:
* {namespace yvh=VENDOR\Yourext\ViewHelpers}
* or in ext_tables.php:
* $GLOBALS['TYPO3_CONF_VARS']['SYS']['fluid']['namespaces']['yvh'] = ['VENDOR\Yourext\ViewHelpers'];
*
* <yvh:myError key="yourObj.somefield" flattenedErrors="{validationResults.flattenedErrors}"/>
*/
class MyErrorViewHelper extends AbstractViewHelper
{
protected $escapeOutput = false;
public function initializeArguments()
{
parent::initializeArguments();
$this->registerArgument('key', 'sring', 'Name of the field for which errors should be displayed');
$this->registerArgument('flattenedErrors', 'mixed', 'Flatenned errors if any');
}
public function render()
{
$flattenedErrors = $this->arguments['flattenedErrors'];
if (is_null($flattenedErrors)) return null;
// DebuggerUtility::var_dump($flattenedErrors);
$key = $this->arguments['key'];
if (is_array($flattenedErrors) && array_key_exists($key, $flattenedErrors)) {
$errMsg = $flattenedErrors[$key][0]->getMessage();
return "<div class='my-very-own-error-class'>$errMsg</div>";
}
return null;
}
}
Remember, that you'll need to wrap your fields with <f anyway to get the flattenErrors array;
{namespace yvh=VENDOR\Yourext\ViewHelpers}
<f:form.validationResults>
<div>
<label for="name">Name (required)</label>
<f:form.textfield property="name"/>
<yvh:myError key="yourObj.name" flattenedErrors="{validationResults.flattenedErrors}"/>
</div>
<div>
<label for="slug">Slug (required)</label><br/>
<f:form.textfield property="slug"/>
<yvh:myError key="yourObj.slug" flattenedErrors="{validationResults.flattenedErrors}"/>
</div>
<div>
<label for="buildYear">Year of build (required)</label>
<f:form.textfield property="buildYear"/>
<yvh:myError key="yourObj.buildYear" flattenedErrors="{validationResults.flattenedErrors}"/>
</div>
</f:form.validationResults>

Creating div in vue for loop

I'd like to do v-for loop for creating a div. I'm doing a minesweeper game and here is my code :
<div class="grid">
<div class="square"
v-for="(square, index) in squares"
:id="index"
:key="index"
:class="squares[index]"
#click="clicked(square, index)"
>
</div>
</div>
'Squares' in this code is an array with shuffled classes 'bomb' or 'empty'. I know that it's wrong because after I click on random square I get only this class from te 'squares' array. What should be there instead of this 'squares' array in v-for. I want to get whole with classes, attributes etc. because later I have to use 'classList' 'contains' etc.
Sorry, maybe I'm completly wrong and talking bullshit, but I started with vue 3 weeks ago.
Here is the method clicked which I want to use
clicked(square) {
if(this.isGameOver) return;
if(square.classList.contains('chechked') || square.classList.contains('flag')) return
if(square.classList.contains('bomb')) {
this.gameOver(square);
} else {
let total = square.getAttribute('data');
if(total != 0) {
square.classList.add('checked');
square.innerHTML = total;
return
}
}
square.classList.add('checked');
}
You want to access the div element but you are passing the object in the method and you are asking for classList into the object (that does not have it). You should query the element instead.
Change the #click handler in your component to:
#click="clicked"
and your method to:
clicked(event) {
let square = event.target;
console.log(square);
console.log(square.classList);
...

How to keep track of an array change in Vue.js when the index is a dynamic value?

I am building an app using Node.js and Vue.
My DATA for the component is the following:
data() {
return {
campaign: {
buses: [],
weeks: [
{
oneWayBuses: [],
returnBuses: []
}
]
},
busesMap: {
// id is the bus ID. Value is the index in the campaign.buses array.
},
};
},
I fill the buses and weeks array in MOUNTED section in two separate methods after getting the data from the server:
responseForWeeks => {
responseForWeeks.forEach(
week => this.campaign.weeks.push(week);
)
}
responseForBuses => {
responseForBuses.forEach(
bus => this.campaign.buses.push(bus);
// Here I also fill the busesMap to link each week to its bus index in the array of buses
this.busesMap[bus.id] = this.campaign.buses.length - 1;
)
}
So the idea is that my busesMap looks like busesId keys and index values:
busesMap = {
'k3jbjdlkk': 0,
'xjkxh834b': 1,
'hkf37sndd': 2
}
However, when I try to iterate over weeks, v-if does not update so no bus info is shown:
<ul>
<li
v-for="(busId, index) in week.oneWayBuses"
:key="index"
:item="busId"
>
<span v-if="campaign.buses[busesMap.busId]">
<strong>{{ campaign.buses[busesMap.busId].busLabel }}</strong>
leaves on the
<span>{{ campaign.buses[busesMap.busId].oneWayDepartureDate.toDate() | formatDate }}</span>
</span>
</li>
</ul>
On the other side, if I shorten the v-if condition to campaign.buses, then I get into the condition but campaign.buses[busesMap.busId] is still undefined, so I get an ERROR trying to display busLabel and oneWayDepartureDate
I've read vue in depth documentation, but couldn't come up with a resolution.
Any gotchas you can find out?
Try this:
async mounted(){
await responseForWeeks
await responseForBuses
responseForWeeks => {
responseForWeeks.forEach(
week => this.campaign.weeks.push(week);
)
}
// this is partial since it is what you provided
responseForBuses => {
responseForBuses.forEach(
bus => this.campaign.buses.push(bus);
// Here I also fill the busesMap to link each week to its bus index in the array of buses
this.busesMap[bus.id] = this.campaign.buses.length - 1;
)
}
}
Basically you want to make sure that before your component loads your data is in place. You can also create computed properties which will force re rendering if dependencies are changed and they are in the dom.
Actually, the problem was indeed in the HTML.
When trying to access the object keys, better use [] intead of a dot .
Final HTML result would be as follows:
<ul>
<li
v-for="(busId, index) in week.oneWayBuses"
:key="index"
:item="busId"
>
<span v-if="campaign.buses[[busesMap[busId]]]">
<strong>{{ campaign.buses[busesMap[busId]].busLabel }}</strong>
leaves on the
<span>{{ campaign.buses[busesMap[busId]].oneWayDepartureDate.toDate() | formatDate }}</span>
</span>
</li>
</ul>
What was happening is that previously campaign.buses[busesMap.busId] did not exist, thus not rendering anything. Solved to campaign.buses[busesMap[busId]]. Also used claudators for the displayed moustache sintach.
Hope it helps someone else messing with Objects!

Returning $key in AngularFire 5

I have the following code that works with AngularFire 5:
export class NavigationComponent {
items: Observable<any[]>;
constructor(db: AngularFireDatabase) {
this.items = db.list('/pages', ref => {
let query = ref.limitToLast(100).orderByChild('sortOrder');
return query;
}).valueChanges();
}
}
But now need to return item.$key which apparently is no longer returned by default in AngularFire 5. I see mention in the migration guide of needing to "map" this, but can't seem to get the right syntax working on the above code.
Update: followed the advice below, and it seemed to have worked but there appears to be some difference still between the behavior between old and new.
<nav class="nav-standard">
<app-logo></app-logo>
<div class="nav-dropdown">
<ul *ngFor="let item of items | async | filter : 'parent' : '00000000-0000-0000-0000-000000000000'" class="nav-dropdown">
<li>
<a mat-button class="mat-button" href="{{item.path}}" data-id="{{item.key}}" target="{{item.target}}" title="{{item.tooltip}}" [attr.data-sort]="item.sortOrder" *ngIf="item.content; else elseBlock">{{item.menuText}}</a>
<ng-template #elseBlock>
<a mat-button class="mat-button" data-id="{{item.key}}" target="{{item.target}}" title="{{item.tooltip}}">{{item.menuText}}</a>
</ng-template>
<ul class="nav-dropdown-content">
<li *ngFor="let childItem of items | async | filter : 'parent' : item.key" class="">
<a class="mat-button" href="{{childItem.path}}" data-id="{{childItem.key}}" target="{{childItem.target}}" title="{{childItem.tooltip}}" [attr.data-sort]="childItem.sortOrder">{{childItem.menuText}}</a>
</li>
</ul>
</li>
</ul>
</div>
</nav>
The nested *ngFor never seems to fire, whereas before it did. It appears that items in the nested *ngFor is null ?? I found if I create another Observable in my component called childItems and assign duplicate logic to that, it works okay -- but to me that feels dirty and wrong. How can I get the data in the observable to persist long enough to use it in the nested *ngFor ?
item key information isn't 'free' anymore upon the new AngularFire API changes. Instead of use '.valueChanges()' to turn reference into Observable, you can use '.snapshotChanges()':
.snapshotChanges()
.map(changes => {
return changes.map(change => ({key: change.payload.key, ...change.payload.val()}));
})
from then on you can reference item key by using 'item.key' (notice that you cannot use '$key').

Create a method/function to use within a view

How can we write a function to use in a *.cshtml page. We used to be able to use #helper or #function within the view. How do we do this? For instance, I would like to write a recursive function to show all configuration values. How could I do this?
<dl>
#foreach(var k in config.GetSubKeys())
{
<dt>#k.Key</dt>
<dd>#config.Get(k.Key)</dd>
#* TODO How can we make this a helper function/recursive? *#
#foreach(var sk in config.GetSubKey(k.Key).GetSubKeys())
{
<dt>#sk.Key</dt>
<dd>#config.Get(sk.Key)</dd>
}
}
</dl>
I imagine that we need to add a dependency in project.json and then opt-in to using it in Startup.cs.
Quick and dirty using razor views assuming your view component provides a recursive model.
Component view:
#model YourRecursiveDataStructure
<ul class="sidebar-menu">
<li class="header">MAIN NAVIGATION</li>
#foreach (var node in Model.RootNodes)
{
#Html.Partial("~/YourPath/RenderElement.cshtml", node)
}
</ul>
Render element in a view :
#model YourRecursiveNode
<li>
<a href="#Model.Href">
<span>#Model.Display</span>
</a>
#Html.Partial("~/YourPath/RenderChildren.cshtml", Model)
</li>
Then loop node's children in another view:
#model YourRecursiveNode
#if (Model.HasChildren)
{
<ul>
#foreach (var child in Model.Children)
{
#Html.Partial("~/YourPath/RenderElement.cshtml", child)
}
</ul>
}
Referring to a few design discussions that we only have glimpses of online, #helper was removed for design reasons; the replacement is View Components.
I'd recommend a View Component that looked like the following:
public class ConfigurationKeysViewComponent : ViewComponent
{
private readonly IConfiguration config;
public ConfigurationKeysViewComponent(IConfiguration config)
{
this.config = config;
}
public IViewComponentResult Invoke(string currentSubKey = "")
{
return View(new ConfigurationData
{
Key = currentSubKey,
Value = config.Get(currentSubKey),
SubKeys = config.GetSubKey(currentSubKey).GetSubKeys().Select(sk => sk.Key)
});
}
}
Your ViewComponent's View would then be relatively simple:
<dt>#Model.Key</dt>
<dd>#config.Get(Model.Key)</dd>
#foreach (var sk in Model.SubKeys)
{
#Component.Invoke("ConfigurationKeys", sk)
}
You could then invoke it from your root view as follows:
#Component.Invoke("ConfigurationKeys")
Disclaimer: I wrote this in the SO editor, there may be compiler errors. Also, I'm uncertain if View Components support default parameters - you may need to add a default "" to the root view's call to the view component.
Alternatively, if this is just debugging code, you can unwrap your recursiveness by using a Stack<>.