Dynamically change material-dropdown-selection options using searchbox with keyup event in AngularDart - angular-dart

I have a dropdown menu with a search box to select out of about 1000 Options which are hierarchical. I begin to show top categories 1, 2, 3 in the dropdown. If you enter, say '1' a filter I implemented will now show subcategories 11, 12, .. of 1 only. This filter is called using keyup event on material-select-searchbox. I get a 'Expression has changed after it was checked'.
I suspect I get this error because I cannot modify parent input params just like that. I tried to solve the problem implementing the event handler in the parent component, modifying a parent variable (StringSelectionOptions categories in categories-selector.dart) that is an input to the material-select-searchbox (in categories-selector.html) - but this does not work.
I also tried fiddling around with #ViewChild but - without proper results.
I heard about StreamController and EventBusses, but I am not sure how to use these to solve my problem using library components (i.e. MaterialDropdownSelectComponent and MaterialSelectSearchboxComponent).
categories.dart
class Categories {
static final List<Category> categories = [
["1", "", "Category 1"]
["2", "", "Category 2"]
["11", "1", "Category 11 - Subcategory of 1"]
["12", "1", "Category 12 - Subcategory of 1"]
["21", "2", "Category 21 - Subcategory of 2"]
["22", "2", "Category 22 - Subcategory of 2"]
//... a thousand more ...
].map(
(c) => Category(c.first, els[1], els.first + " " + els.last)
).toList();
List<Category> filter(String searchterm){
return categories.where( (nc) =>
nc.parent == parent ).map((nc) => nc).toList();
}
}
class Category implements HasUIDisplayName {
String code;
String parent;
String label;
Category(this.code, this.parent, this.label);
#override
String toString() => label;
}
categories-selector.dart: imports
import 'dart:async';
import 'package:angular/angular.dart';
import 'package:angular_components/material_button/material_button.dart';
import 'package:angular_components/material_icon/material_icon.dart';
import 'package:angular_components/material_select/material_dropdown_select.dart';
import 'package:angular_components/material_select/material_select_searchbox.dart';
import 'package:angular_components/model/selection/string_selection_options.dart';
import 'package:right_scenario_explorer/src/activity_editor/scope_collapse_mixin.dart';
import 'package:right_scenario_explorer/src/company/activity.dart';
import 'package:right_scenario_explorer/src/company/emission.dart';
import 'package:right_scenario_explorer/src/utils/formatter.dart';
import 'package:right_scenario_explorer/src/utils/nace_codes.dart';
categories-selector.dart: code
#Component(
exports: [Formatter, Categories],
selector: "categories-selector",
templateUrl: "categories-selector.html",
directives: [
coreDirectives,
MaterialDropdownSelectComponent,
MaterialSelectSearchboxComponent,
MaterialButtonComponent,
MaterialIconComponent,
]
)
class CategoriesSelector extends OnInit with {
#Output()
Category selectedCategory;
StringSelectionOptions<Category> categories;
#override
void ngOnInit() {
getCategoryOptions("");
}
void getCategoryOptions(String searchterm) {
Categories categories = new Categories();
var selectedOptions = categories.filter(searchterm);
categoryOptions = StringSelectionOptions(selectedOptions);
}
void categorySearchBoxEventHandler(dynamic event){
getCategoryOptions(event.target.value);
}
void updateSelectedCategory(Category category) {
selectedCategory = category;
}
}
categories-selector.html
<div>
<material-dropdown-select
buttonAriaRole="combobox"
[buttonText]="selectedCategory == null ? 'None' : selectedCategory.toString()"
[options]="categories"
(selection)="updateSelectedCategory($event)"
[listAutoFocus]="false"
[activateFirstOption]="false"
<div header>
<material-select-searchbox
(keyup)="categorySearchBoxEventHandler"
label="Search..."
[filterable]="categories">
</material-select-searchbox>
</div>
</material-dropdown-select>
</div>
I would like to know how I can modify the options of material-dropdown-select from the keyup event of the material-select-searchbox. Any suggestions are much appreciated! Thank you!

You could try material-auto-suggest-input
that has been made to dynamically update options when you enter a value.

Related

lombok delegation pattern not returning all methods in model.json(Json Exporter) for Tabs component

I am using delegation pattern using lombok - hoping all getter method of parent super type will be exported in model.json
This is the sling model I wrote for Tabs component
package com.test.internal.models;
import com.adobe.cq.export.json.ComponentExporter;
import com.adobe.cq.export.json.ContainerExporter;
import com.adobe.cq.export.json.ExporterConstants;
import com.adobe.cq.wcm.core.components.internal.models.v1.TabsImpl;
import com.adobe.cq.wcm.core.components.models.Component;
import com.adobe.cq.wcm.core.components.models.Container;
import com.adobe.cq.wcm.core.components.models.ListItem;
import com.adobe.cq.wcm.core.components.models.Tabs;
import com.adobe.cq.wcm.core.components.models.datalayer.ComponentData;
import lombok.experimental.Delegate;
import org.apache.commons.lang3.StringUtils;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.api.resource.ResourceResolver;
import org.apache.sling.models.annotations.*;
import org.apache.sling.models.annotations.injectorspecific.Self;
import org.apache.sling.models.annotations.injectorspecific.ValueMapValue;
import org.apache.sling.models.annotations.via.ResourceSuperType;
import javax.inject.Inject;
import java.util.List;
#Model(
adaptables = {Resource.class, SlingHttpServletRequest.class},
adapters = {Tabs.class,ComponentExporter.class, ContainerExporter.class, Container.class},
resourceType = Accordion.RESOURCE_TYPE
)
#Exporter(
name = ExporterConstants.SLING_MODEL_EXPORTER_NAME,
extensions = ExporterConstants.SLING_MODEL_EXTENSION
)
public class Accordion implements Tabs {
static final String RESOURCE_TYPE = "test/components/accordion";
#Self #Via(type = ResourceSuperType.class)
#Delegate(types = Tabs.class,excludes = DelegationExclusion.class)
private com.adobe.cq.wcm.core.components.models.Tabs delegate;
#Override
public String getActiveItem() {
return "test";
}
private interface DelegationExclusion { // Here we define the methods we want to override
String getActiveItem();
}
}
I am trying to override the getActiveItem method for Tabs component. But it's not populating the items properly - they are empty in the json - just returning the overridden method in the json -
"accordion": {
"items": [],
"activeItem": "test",
":itemsOrder": [],
":items": {}
}
when I remove this Sling model from the bundle - the OOTB sling model comes into picture and returns everything -
"accordion": {
"id": "accordion-5248ba2449",
"activeItem": "item_1",
":itemsOrder": [
"item_1",
"item_2",
"ctabutton"
],
":items": {
"item_1": {
"gridClassNames": "aem-Grid aem-Grid--12 aem-Grid--default--12",
"columnClassNames": {
"backbutton": "aem-GridColumn aem-GridColumn--default--12"
},
"columnCount": 12,
"allowedComponents": {
"components": [],
"applicable": false
},
":itemsOrder": [
"backbutton"
],
":items": {
"backbutton": {
":type": "test/components/backbutton",
"btnLabel": "View More"
}
},
":type": "test/components/container",
"cq:panelTitle": "FAQ 1"
},
"item_2": {
"gridClassNames": "aem-Grid aem-Grid--12 aem-Grid--default--12",
"columnClassNames": {},
"columnCount": 12,
"allowedComponents": {
"components": [],
"applicable": false
},
":itemsOrder": [],
":items": {},
":type": "test/components/container",
"cq:panelTitle": "FAQ 2"
},
"ctabutton": {
":type": "test/components/ctabutton",
"btnAlignment": "left",
"lightboxId": "sadasdasdasdasdas",
"btnType": "cta-primary",
"btnLabel": "View More",
"openLightbox": "true",
"cq:panelTitle": null
}
},
":type": "test/components/accordion"
}
can someone please let me know what is missing in sling model that it's not returning the items properly
Here is working example from project, should help you.
#Model(
adaptables = SlingHttpServletRequest.class,
adapters = { Tabs.class, ComponentExporter.class, ContainerExporter.class },
resourceType = absModel.TABS_RESOURCE_TYPE,
defaultInjectionStrategy = DefaultInjectionStrategy.OPTIONAL
)
#Exporter(
name = ExporterConstants.SLING_MODEL_EXPORTER_NAME,
extensions = ExporterConstants.SLING_MODEL_EXTENSION
)
#JsonSerialize(as = TabsModel.class)
public class TabsModel implements Tabs {
protected static final String TABS_RESOURCE_TYPE = "myproject/components/tabs";
#Self
private SlingHttpServletRequest request;
#Self
#Via(type = ResourceSuperType.class)
#Delegate(excludes = Excludes.class)
private Tabs tabs;
#Override
public String getExportedType() {
return this.request.getResource().getResourceType();
}
private interface Excludes {
String getExportedType();
}
}

Liferay create a custom PanelCategory for multiple portlet

I use Liferay 7.2 and Liferay IDE (eclipse). I created two separate Liferay admin portlet to creating a view for the database entries. I added in the first portlet "Teachers" a new panel called school with this generated code in application.list Package.
here is the code of - PanelApp.java
#Component(
immediate = true,
property = {
"panel.app.order:Integer=300",
"panel.category.key=" + TeachersPanelCategoryKeys.CONTROL_PANEL_CATEGORY
},
service = PanelApp.class
)
public class TeachersPanelApp extends BasePanelApp {
#Override
public String getPortletId() {
return TeachersPortletKeys.TEACHERS;
}
#Override
#Reference(
target = "(javax.portlet.name=" + TeachersPortletKeys.TEACHERS+ ")",
unbind = "-"
)
public void setPortlet(Portlet portlet) {
super.setPortlet(portlet);
}
}
.
public class TeachersPanelCategoryKeys {
public static final String CONTROL_PANEL_CATEGORY = "Teachers";
}
And here is the code of - PanelCategory.java
#Component(
immediate = true,
property = {
"panel.category.key=" + PanelCategoryKeys.SITE_ADMINISTRATION,
"panel.category.order:Integer=300"
},
service = PanelCategory.class)
public class TeachersPanelCategory extends BasePanelCategory {
#Override
public String getKey() {
return TeachersPanelCategoryKeys.CONTROL_PANEL_CATEGORY;
}
#Override
public String getLabel(Locale locale) {
return LanguageUtil.get(locale, "School");
}}
And here is the code of portlet.java
#Component(
immediate = true,
property = {
"com.liferay.portlet.add-default-resource=true",
"com.liferay.portlet.display-category=category.hidden",
"com.liferay.portlet.header-portlet-css=/css/main.css",
"com.liferay.portlet.layout-cacheable=true",
"com.liferay.portlet.private-request-attributes=false",
"com.liferay.portlet.private-session-attributes=false",
"com.liferay.portlet.render-weight=50",
"com.liferay.portlet.use-default-template=true",
"javax.portlet.display-name=Teachers",
"javax.portlet.expiration-cache=0",
"javax.portlet.init-param.template-path=/",
"javax.portlet.init-param.view-template=/view.jsp",
"javax.portlet.name=" + TeachersPortletKeys.TEACHERS,
"javax.portlet.resource-bundle=content.Language",
"javax.portlet.security-role-ref=power-user,user",
},
service = Portlet.class
)
public class TeachersPortlet extends MVCPortlet {
// some code to get entries from db
#Override
public void doView(final RenderRequest renderRequest, final RenderResponse renderResponse)
throws IOException, PortletException {
// some code
Now I want to add the second created portlet "Students" under the same Panel "School". I created it in the same way as "Teachers" but now I have two school panel. As it is shown in the image below.
I just want to display one panel category called school that contain both Teachers and Students in the list.
I do not know how I can think to do that.
As you're implementing a TeachersPanelCategory, I'm assuming you're also implementing a StudentsPanelCategory. From a naming perspective, I'd have expected a SchoolPanelCategory.
I'm currently at a loss of how the ControlPanel portlets actually declare their associated panel, but that place would be where you pick the common "school" panel and use the same spelling for both.
In other words: If you deploy two panels with the same name, I'd expect exactly what you document here. Make sure you're only deploying one of them
Edit: I'd like to know what TeachersPanelCategoryKeys.CONTROL_PANEL_CATEGORY is defined as, and the corresponding (assumed, not shown) StudentsPanelCategoryKeys.CONTROL_PANEL_CATEGORY. Both categories have the same label, but if they have different keys, they'll be different. I'm not sure what happens when you deploy two components with the same key: You should deploy only one.
Edit2: I've missed the code before: You're producing the key to your first category as "Teachers", and the label as "School". I'm assuming that the key for your other category is "Students". Liferay organizes the categories by key - and if the keys are different, then you'll end up with two different categories. Make their key more similar to their name (e.g. create a single SchoolCategory and associate your portlets/panelApps with that:
#Component(
immediate = true,
property = {
"panel.category.key=" + PanelCategoryKeys.SITE_ADMINISTRATION,
"panel.category.order:Integer=300"
},
service = PanelCategory.class)
public class SchoolPanelCategory extends BasePanelCategory {
#Override
public String getKey() {
return "school"; // this is the category that you want to associate with
}
#Override
public String getLabel(Locale locale) {
return LanguageUtil.get(locale, "School");
}
}
and
#Component(
immediate = true,
property = {
"panel.app.order:Integer=300",
"panel.category.key=school" // referencing the category created above
// (use the same for your StudentsPanelApp)
},
service = PanelApp.class
)
public class TeachersPanelApp extends BasePanelApp {
#Override
public String getPortletId() {
return TeachersPortletKeys.TEACHERS;
}
#Override
#Reference(
target = "(javax.portlet.name=" + TeachersPortletKeys.TEACHERS+ ")",
unbind = "-"
)
public void setPortlet(Portlet portlet) {
super.setPortlet(portlet);
}
}
(See the one-line comments within the code for the critical lines. Replace with proper constants if you like)

Displaying component property from record provider is undefined

I am creating an ionic app. In this modal, I want a select with options populated from my Provider (called recordProvider). categories should hold an array of objects from the recordProvider.
The name property of these objects is what goes in the select.
I am able to log categories immediately after it is assigned from recordsProvider and it shows all the proper records perfectly. However, the next line logs the length at 0. Most importantly, the UI errors with "Cannot read property 'name' of undefined"
Why does categories have this inconsistent value?
If it is just an issue of timing and categories will have the correct data in a moment, why isn't it updated in the UI? Isn't that the whole get with Angular?
How do I fix it?
Modal ts
import { Component } from '#angular/core';
import { IonicPage, NavController, NavParams,ViewController } from 'ionic- angular';
import { RecordsProvider } from './../../providers/records/records';
#IonicPage()
#Component({
selector: 'page-add-modal',
templateUrl: 'add-modal.html',
})
export class AddModalPage {
categories:object[] = [];
constructor(public navCtrl: NavController, public navParams: NavParams, public viewCtrl : ViewController, public recordProvider: RecordsProvider) {
}
ngOnInit() {
this.categories = this.recordProvider.getAllExpenseCategories();
console.log(this.categories);
console.log(this.categories.length);
}
public closeModal(){
this.viewCtrl.dismiss();
}
}
Modal HTML
<ion-content padding>
<h1 (click)="getCat()">Hello</h1>
<p>{{categories[0].name}}</p>
<ion-item>
<ion-label>categories</ion-label>
<ion-select>
<ion-option ng-repeat="obj of categories" value="{{obj.name}}">{{obj.name}}</ion-option>
</ion-select>
</ion-item>
</ion-content>
EDIT RecordsProvider
import { HttpClient } from '#angular/common/http';
import { Injectable } from '#angular/core';
import { Storage } from '#ionic/storage';
#Injectable()
export class RecordsProvider {
getAllExpenseCategories(){
let categories = [];
this.storage.forEach( (value, key, index)=>{
if(key.indexOf("Exp") == 0){
categories.push(value);
}
});
return categories;
}
}
Ionic Storage (localForage) uses async API, so I would make sure you write your methods with it accordingly, I would re-write the getAllExpenseCategories to leverage promise which is returned by storage:
getAllExpenseCategories(){
let categories = [];
this.storage.forEach( (value, key, index)=>{
if(key.indexOf("Exp") == 0){
categories.push(value);
}
}).then(()=>{
return categories;
})
}
In your case it seems like your method was returning empty array to the component, before storage completed its forEach cycle.
Let me know if this helped

RadListView doesn't display items

I am having much difficulties implementing RadListView in my application. According to documentation the ListView accepts items as an ObservableArray, but regardless of whether that or plain array is used, items are not being displayed. I prepared a sample app in the Playground
https://play.nativescript.org/?template=play-ng&id=39xE2X&v=11
Component is like this:
import { Component, ViewChild, OnInit, ChangeDetectorRef, ElementRef } from "#angular/core";
import { GestureTypes, PanGestureEventData, TouchGestureEventData } from "tns-core-modules/ui/gestures";
import { ObservableArray } from "tns-core-modules/data/observable-array";
#Component({
selector: "ns-app",
moduleId: module.id,
templateUrl: "./app.component.html",
})
export class AppComponent implements OnInit {
public gridData = []; //new ObservableArray<any>();
constructor(
private cd: ChangeDetectorRef
) {
this.feedTestData();
}
ngOnInit() {
}
ngAfterViewInit() {
}
public detectChanges() {
this.cd.detectChanges();
}
private feedTestData() {
let data = [
{
description: 'line 1',
},
{
description: 'line 2',
},
];
this.gridData.splice(0, 0, data);
}
}
and template like this:
<GridLayout tkExampleTitle tkToggleNavButton>
<RadListView [items]="gridData">
<ng-template tkListItemTemplate let-item="item">
<StackLayout orientation="vertical">
<Label [text]="description"></Label>
</StackLayout>
</ng-template>
</RadListView>
</GridLayout>
What's perplexing me even more is that in my real application RadListView displays something, but does it on its own, completely ignoring my template. I can literally leave nothing inside RadListView, but it would still display items
I have updated your playground and it is working now.
https://play.nativescript.org/?template=play-ng&id=39xE2X&v=12
You were trying to access the description in ng-template while it should be item.description
<Label [text]="item.description"></Label>
Also for the testing purpose, I am creating an Onservable array from your data. this.gridData = new ObservableArray(data); in your feedTestData function.

Exchange Data between multi step forms in Angular2: What is the proven way?

I can imagine following approaches to exchange Data between multi step forms:
1) Create a component for each form step and exchange data between components over #input, #output (e.g. you cannot change from step5 to 2)
2) Use the new property data in the new router (see here) (e.g. you cannot change from step5 to 2))
3) A shared Service (Dependency Injection) to store data (Component Interaction) (e.g. you can change from step5 to 2)
4) New rudiments with #ngrx/store (not really experienced yet)
Can you give some "gained experience values", what do you use and why?
See my edit below.
Using SessionStorage is not strictly the 'angular' way to approach this in my opinion—a shared service is the way to go. Implementing routing between steps would be even better (as each component can have its own form and different logic as you see fit:
const multistepRoutes: Routes = [
{
path: 'multistep',
component: MultistepComponent,
children: [
{
path: '',
component: MultistepBaseComponent,
},
{
path: 'step1',
component: MultistepStep1Component
},
{
path: 'step2',
component: MultistepStep2Component
}
]
}
];
The service multistep.service can hold the model and implement logic for components:
import { Injectable, Inject } from '#angular/core';
import { Router } from '#angular/router';
#Injectable()
export class MultistepService {
public model = {};
public baseRoute = '/multistep';
public steps = [
'step1',
'step2'
];
constructor (
#Inject(Router) public router: Router) { };
public getInitialStep() {
this.router.navigate([this.baseRoute + '/' + this.steps[0]]);
};
public goToNextStep (direction /* pass 'forward' or 'backward' to service from view */): any {
let stepIndex = this.steps.indexOf(this.router.url.split('/')[2]);
if (stepIndex === -1 || stepIndex === this.steps.length) return;
this.router.navigate([this.baseRoute + '/' + this.steps[stepIndex + (direction === 'forward' ? 1 : -1)]]);
};
};
Good luck.
EDIT 12/6/2016
Actually, now having worked with the form API for a while I don't believe my previous answer is the best way to achieve this.
A preferrable approach is to create a top level FormGroup which has each step in your multistep form as it's own FormControl (either a FormGroup or a FormArray) under it's controls property. The top level form in such a case would be the single-source of truth for the form's state, and each step on creation (ngOnInit / constructor) would be able to read data for its respective step from the top level FormGroup. See the pseudocode:
const topLevelFormGroup = new FormGroup({
step1: new FormGroup({fieldForStepOne: new FormControl('')}),
step2: new FormGroup({fieldForStepTwo}),
// ...
});
...
// Step1Component
class Step1Component {
private stepName: string = 'step1';
private formGroup: FormGroup;
constructor(private topLevelFormGroup: any /* DI */) {
this.formGroup = topLevelFormGroup.controls[this.stepName];
}
}
Therefore, the state of the form and each step is kept exactly where it should be—in the form itself!
Why not use session storage? For instance you can use this static helper class (TypeScript):
export class Session {
static set(key:string, value:any) {
window.sessionStorage.setItem(key, JSON.stringify(value));
}
static get(key:string) {
if(Session.has(key)) return JSON.parse(window.sessionStorage[key])
return null;
}
static has(key:string) {
if(window.sessionStorage[key]) return true;
return false;
}
static remove(key:string) {
Session.set(key,JSON.stringify(null)); // this line is only for IE11 (problems with sessionStorage.removeItem)
window.sessionStorage.removeItem(key);
}
}
And using above class, you can put your object with multi-steps-forms data and share it (idea is similar like for 'session helper' in many backend frameworks like e.g. php laravel).
The other approach is to create Singleton service. It can look like that (in very simple from for sake of clarity) (I not test below code, I do it from head):
import { Injectable } from '#angular/core';
#Injectable()
export class SessionService {
_session = {};
set(key:string, value:any) {
this._session[key]= value; // You can also json-ize 'value' here
}
get(key:string) {
return this._session[key]; // optionally de-json-ize here
}
has(key:string) {
if(this.get(key)) return true;
return false;
}
remove(key:string) {
this._session[key]=null;
}
}
And then in your main file where you bootstrap application:
...
return bootstrap(App, [
...
SessionService
])
...
And the last step - critical: When you want to use you singleton service in your component - don't put int in providers section (this is due to angular2 DI behavior - read above link about singleton services). Example below for go from form step 2 to step 3:
import {Component} from '#angular/core';
import {SessionService} from './sessionService.service';
...
#Component({
selector: 'my-form-step-2',
// NO 'providers: [ SessionService ]' due to Angular DI behavior for singletons
template: require('./my-form-step-2.html'),
})
export class MyFormStep2 {
_formData = null;
constructor(private _SessionService: SessionService) {
this._formData = this._SessionService.get('my-form-data')
}
...
submit() {
this._SessionService.set('my-form-data', this._formData)
}
}
It should looks like that.