There are actually two challenges I face. I am looping through an array of values and need to
set a class name depending on an observable variable from a child
component.
reevaluate the class as soon as the child variable changes.
location.component.ts
import { Component, Input } from '#angular/core';
import { BusinessLocation, SpecialDays, RegularHours } from './location';
import { DbService } from './db-service.component';
#Component({
selector: '[data-locations]',
templateUrl: 'app/location.component.html',
providers: [DbService]
})
export class LocationComponent {
locations:BusinessLocation[];
selectedLocationId:Number;
constructor(private api:DbService){}
isOpenOnDay(day):Boolean {
let _weekDay = day.getDay();
let _retour = false;
this.locations.forEach(loc => {
if ( loc.id == this.selectedLocationId && loc.regularHours.weekDay == _weekDay ) {
_retour = true;
}
});
this.locations.forEach(loc => {
if ( loc.id == this.selectedLocationId && loc.specialDays.singleDate.getDay() == _weekDay) {
_retour = true;
}
});
return _retour;
}
getLocation():Number {
return this.selectedLocationId;
}
setLocation(id):void {
this.selectedLocationId = id;
}
getLocations():void {
this.api.getLocations().subscribe(
locations => {
this.locations = locations as BusinessLocation[];
this.setLocation(this.locations[0].id);
}
);
}
}
a snippet from db-services.component.ts
getLocations():Observable<BusinessLocation[]> {
return this.http.get(this.apiUrl + '/get_locations.php')
.map(response => response.json().data as BusinessLocation[]);
}
}
it all works fine. However, here is where the challenge is. The parent component initiates the locations, but it also needs to know what location is selected right now. Here is the month.component.html
<span class="location-container" #location data-locations><span class="loading">Loading locations...</span></span>
<div *ngFor="let day of week.days" class="day" data-can-drop="day"
[class.today]="isToday(day)"
[class.in-other-month]="day.getMonth() != jsMonth"
[class.is-closed]="!isOpenAtLocation(day)">
<div class="day-marker"></div>
<span class="day-date">{{day | date:'d'}}</span>
<span *ngIf="checkMonth(day)" class="day-month">{{months[day.getMonth()]}}</span>
</div>
and a snippet from month.component.ts is
#ViewChild('location') locationComponent:LocationComponent;
isOpenAtLocation(day):Boolean {
return this.locationComponent.isOpenOnDay(day);
}
ngOnInit(): void {
this.locationComponent.getLocations();
}
The error I get is pretty straightforward and totally understandable:
Subscriber.ts:238 TypeError: Cannot read property 'forEach' of undefined
at LocationComponent.isOpenOnDay (location.component.ts:25)
at MonthComponent.isOpenAtLocation (month.component.ts:176)
And this is just about Challenge 1. The Challenge 2 has not been even addressed yet.
I just can't wrap my head around it. >_<
Thanks.
Well, this was a bad joke on my side. First, this is a reminder, that a change of a property of an object will reflect itself in DOM if there is a binding available. So going with [class.isOpenOnDay]="day.isOpenAtLocation" would be totally sufficient, where day is an object and isOpenAtLocation is its property. Even if it is not set initially (meaning it is null) and will be updated in the future – it is all good. This is basically how NG works (and has worked all the time). Silly me.
The other problem – changing the value depending on a child component variable – has been solved by emitting events (from child), listening to the events (in parent) and resetting the property isOpenAtLocation again.
So the updated child component location.component.ts has been updated like this:
#Output() locationChanged = new EventEmitter<Number>();
setLocation(id):void {
this.selectedLocationId = id;
this.locationChanged.emit(this.selectedLocationId);
}
The view for the location component now has this line:
<select (change)="setLocation($event.target.value)">
<option *ngFor="let loc of locations" value="{{loc.id}}">{{loc.longName}}</option>
</select>
The parent component's view is bound to the event like this:
<span class="location-container" #location data-locations (locationChanged)="onLocationChange($event)"><span class="loading">Loading locations...</span></span>
And the parent month.component.ts itself has two more methods:
onLocationChange(event) {
if ( this.selectedLocationId != event ) {
this.selectedLocationId = event;
this.setLocation();
this.dispatchResize();
}
}
setLocation():void {
if ( this.selectedLocationId >= 0) {
for ( let i = 0; i < this.weeks.length; i++) {
let _week = this.weeks[i];
_week.forEach(_day => {
let _isOpen = this.locationComponent.isOpenOnDay(_day.date);
_day['isOpenOnDay'] = _isOpen.isOpenOnDay;
_day['isSpecialDay'] = _isOpen.isSpecialDay;
_day['dayHours'] = _isOpen.dayHours;
});
}
}
}
As one can see, I have added even more dynamically checked properties, not just isOpenOnDay, but also isSpecialDay and dayHours, which are not yet defined initially, but are set as soon as the data is available – and are reflected in view as soon as they change.
Basic stuff, actually. Still might be helpful to some NG2 noob like me.
Related
For my administration, i develop a new button Field to display on the index page of my userCrudController.
When clicked, it should launch an ajax action.
So to do this js logic, i create this stimulus controller.
import { Controller } from '#hotwired/stimulus';
import { Toast } from 'bootstrap';
import { createToast, createSpinner } from '../js/customs';
/*
* Ajax button Field Logic
*/
export default class extends Controller {
static targets = [];
sucessToast = null;
failedToast = null;
disable = false;
connect() {
// Search if Toast Element is already create on the page
// if not, we create it
let sucessToast = document.querySelector('#successAjaxToast');
if(!sucessToast) {
createToast('successAjaxToast', 'bg-success', 4000);
sucessToast = document.querySelector('#successAjaxToast');
}
let failedToast = document.querySelector('#failedAjaxToast');
if(!failedToast) {
createToast('failedAjaxToast', 'bg-danger', 4000);
failedToast = document.querySelector('#failedAjaxToast');
}
this.sucessToast = new Toast(sucessToast);
this.failedToast = new Toast(failedToast);
}
runAjax(event) {
let url = event.params.ajaxUrl;
let sucessToast = document.querySelector('#successAjaxToast');
let failedToast = document.querySelector('#failedAjaxToast');
let saveInnerHtml = event.target.innerHTML;
let dims = event.target.getBoundingClientRect();
if (!this.disable) {
this.disable = true;
event.target.innerHTML = createSpinner('').outerHTML;
event.target.style.width = dims.width + 'px';
fetch(url)
.then(obj => obj.json())
.then(data => {
if (data.error) {
throw data.errorMessage;
}
if (data.successMessage) {
sucessToast.querySelector('.toast-body').innerHTML = data.successMessage;
} else {
sucessToast.querySelector('.toast-body').innerHTML = 'Succès';
}
this.sucessToast.show();
})
.catch(e => {
failedToast.querySelector('.toast-body').innerHTML = e;
this.failedToast.show();
console.warn(e)
})
.finally(() => {
this.disable = false;
event.target.innerHTML = saveInnerHtml;
event.target.style.width = 'auto';
})
;
}
}
}
The problem is that I import Toast from bootstrap.
And that makes me end up with two bootstraps loaded on my page.
And so my dropdown action buttons, didn't open anymore when i click on '...' (because the action is launch twice)
I tried to override the index template in order to don't load easyadmin "app.js"
It's work,bootstrap is load only once, but of course, this solution make me lost some js logic, like "click on delete" button who don't work anymore.
So this is not a real good solution.
Somebody know how i could use bootstrap Toast in my stimulus controller without loading twice bootstrap js logic?
Thank for help :)
I was taking a look at this :
tornadofx
and tried to expand on it with database connection and little more options, (not all of them make sense, but its just playing in a sandbox).
Even though table can be directly edited and the data will persist in database, i did try to do edit through text fields too. actual table editing would happen through different view and not table itself, as i said its just example.
Database used is Jetbrains Exposed.
object Categories : IntIdTable() {
val name = varchar("name", 64).uniqueIndex()
val description = varchar("description", 128)
}
class Category(id: EntityID<Int>) : IntEntity(id) {
companion object : IntEntityClass<Category>(Categories)
var name by Categories.name
var description by Categories.description
override fun toString(): String {
return "Category(name=\"$name\", description=\"$description\")"
}
}
now controller looks something like this, functions are just rudimentary and picked as an example.
typealias ModelToDirtyState = Map.Entry<CategoryModel, TableColumnDirtyState<CategoryModel>>
class CategoryModel() : ItemViewModel<Category>() {
val name: SimpleStringProperty = bind(Category::name)
val description: SimpleStringProperty = bind(Category::description)
}
class DBController : Controller() {
val categories: ObservableList<CategoryModel> by lazy {
transaction {
SchemaUtils.create(Categories)
Category.all().map {
CategoryModel().apply {
item = it
}
}.observable()
}
}
init {
Database.connect(
"jdbc:mysql://localhost:3306/test", driver = "com.mysql.cj.jdbc.Driver",
user = "test", password = "test"
)
TransactionManager.manager.defaultIsolationLevel = Connection.TRANSACTION_SERIALIZABLE
}
fun deleteCategory(model: CategoryModel) {
runAsync {
transaction {
model.item.delete()
}
}
categories.remove(model)
}
fun updateCategory(model: CategoryModel) {
transaction {
Categories.update {
model.commit()
}
}
}
fun commitDirty(modelDirtyMappings: Sequence<ModelToDirtyState>) {
transaction {
modelDirtyMappings.filter { it.value.isDirty }.forEach {
it.key.commit()
println(it.key)// commit value to database
it.value.commit() // clear dirty state
}
}
}
Just to quickly comment on controller, delete method works as "intended" however the update one does not, it does not work in sense that after using delete item is remove both from database and tableview(underlying list) itself, and when i do update its not, now i know the reason, i call remove manually on both database and list, now for update perhaps i could do change listener, or maybe tornadofx can do this for me, i just cant set it up to do it. Following code will make things clearer i think.
class CategoryEditor : View("Categories") {
val categoryModel: CategoryModel by inject()
val dbController: DBController by inject()
var categoryTable: TableViewEditModel<CategoryModel> by singleAssign()
var categories: ObservableList<CategoryModel> by singleAssign()
override val root = borderpane {
categories = dbController.categories
center = vbox {
buttonbar {
button("Commit") {
action {
dbController.commitDirty(categoryTable.items.asSequence())
}
}
button("Roll;back") {
action {
categoryTable.rollback()
}
}
// This model only works when i use categorytable.tableview.selected item, if i use categoryModel, list gets updated but not the view itself
// Question #1 how to use just categoryModel variable without need to use categorytable.tableview.selecteditem
button("Delete ") {
action {
val model = categoryTable.tableView.selectedItem
when (model) {
null -> return#action
else -> dbController.deleteCategory(model)
}
}
}
//And here no matter what i did i could not make the view update
button("Update") {
action {
when (categoryModel) {
null -> return#action
else -> dbController.updateCategory(categoryModel)
}
categoryTable.tableView.refresh()
}
}
}
tableview<CategoryModel> {
categoryTable = editModel
items = categories
enableCellEditing()
enableDirtyTracking()
onUserSelect() {
//open a dialog
}
//DOES WORK
categoryModel.rebindOnChange(this) { selectedItem ->
item = selectedItem?.item ?: CategoryModel().item
}
// Question #2. why bindSelected does not work, and i have to do it like above
//DOES NOT WORK
// bindSelected(categoryModel)
//
column("Name", CategoryModel::name).makeEditable()
column("Description", CategoryModel::description).makeEditable()
}
}
right = form {
fieldset {
field("Name") {
textfield(categoryModel.name)
}
}
fieldset {
field("Description") {
textfield(categoryModel.description)
}
}
button("ADD CATEGORY") {
action {
dbController.addCategory(categoryModel.name.value, categoryModel.description.value)
}
}
}
}
}
I apologize for huge amount of code, also in last code snipped i left questions in form of comments where i fail to achive desired results.
I am sure i am not properly binding code, i just dont see why, also i sometimes use one variable to update data, my declared one "categoryModel" and sometimes i use tableview.selecteditem, it just seems hacky and i cant seem to grasp way.
Thank you!
I am converting a custom element dropdown over to lit-element. The way the existing element shows the dropdown options is by setting an expanded boolean attribute on the element, and the options are shown/hidden via css:
my-element:not([expanded]) .options-container {
display: none;
}
my-element[expanded] .options-container {
display: block;
}
The component doesn't need to do any rerenders because the logic is all in the css.
How can I achieve this behavior with lit-element, and not rerender the component? Rerendering can be costly if there are a lot of dropdown options.
I have tried implementing a shouldUpdate that returns false if only expanded has changed - but this causes lit-element not to reflect expanded to the attribute when set via a property, which is necessary in order to show/hide via css.
This is what I have, which doesn't work:
class MyDropdown extends LitElement {
static get properties() {
return {
expanded: { type: Boolean, reflect: true },
...
};
}
shouldUpdate(changedProperties) {
if (changedProperties.has('expanded') && changedProperties.size === 1) {
return false;
}
return true;
}
// disable shadow-dom
createRenderRoot() {
return this;
}
}
Note that I am not using shadow dom yet, not sure if that would change the solution. I'm on lit-element 2.2.1.
The idea is to not use LitElement's static properties or #Property decorator. Write your own property implementation like this:
class MyDropdown extends LitElement {
_expanded = false;
get expanded() {
return this._expanded;
}
set expanded(val) {
this._expanded = val;
// Manually setting the property and reflecting attribute.
if (val) {
this.setAttribute('expanded', '');
} else {
this.removeAttribute('expanded');
}
}
// disable shadow-dom
createRenderRoot() {
return this;
}
}
Similarly, you can listen for attributeChangedCallback lifecycle event and adjust _expanded property whenever user changes the attribute and not property.
I have a list page, and click a column to enter the details page. Edit and return. Because using RouteReuseStrategy, it can maintain the list page of the scene remains the same. But I'd like to partially update, However, I don't know how to trigger it.
Here is my RouteReuseStrategy service and most of the same.
export class SimpleReuseStrategy implements RouteReuseStrategy {
_cacheRouters: { [key: string]: any } = {};
shouldDetach(route: ActivatedRouteSnapshot): boolean {
return true;
}
store(route: ActivatedRouteSnapshot, handle: DetachedRouteHandle): void {
this._cacheRouters[route.routeConfig.path] = {
snapshot: route,
handle: handle
};
}
shouldAttach(route: ActivatedRouteSnapshot): boolean {
return !!this._cacheRouters[route.routeConfig.path];
}
retrieve(route: ActivatedRouteSnapshot): DetachedRouteHandle {
return this._cacheRouters[route.routeConfig.path].handle;
}
shouldReuseRoute(future: ActivatedRouteSnapshot, curr:
ActivatedRouteSnapshot): boolean {
return future.routeConfig === curr.routeConfig;
}
}
In your list page component you can listen to route changes:
Detect if the navigation start came from the detail route i.e. /detail/1
if so, get the 'id' param of the route /detail/1
replace that element in the list
// List component
ngOnInit(): void {
this.subscription = this.router.events.subscribe((event) => {
if (event instanceof NavigationStart) {
// checkIfDetailPage and store ID
}
if (event instanceof NavigationEnd) {
// checkIfThisPage and get the ID
// replace element with ID
}
});
}
An alternative approach for detecting something changed (instead of observing the route changes) you could implement Service to which your list component subscribes and your detail component notifies.
I try to update an input value in Angular 2, it works the first time the size value exceeds maxSize, but afterwords it does not work anymore. It seems like when I am setting this.size to some value the UI is not updated, am I overlooking something ?
HTML:
<input type="text" class="form-control" [value]="size" (input)="size = updateValue($event)">
Code:
export class BrushSizePicker {
#Input() minValue;
#Input() maxValue;
#Input() size;
increaseValue(event) {
this.size++;
this.checkValue();
}
decreaseValue(event) {
this.size--;
this.checkValue();
}
updateValue(event) {
this.size = parseInt(event.target.value);
this.checkValue();
return this.size;
}
private checkValue() {
if (this.size > this.maxValue) {
this.size = this.maxValue;
}
if (this.size < this.minValue) {
this.size = this.minValue;
}
}
EDIT:
I logged what happened: checkValue is called every time with the correct input, and it returns the correct value. But the new value is not set into the input field / value field
While it may not solve the problem, the way you have implemented the input event can be simplified. I would have written it like this, side-effect free functions:
updateValue(event) { // The method name with this change is a misnomer
return this.checkValue(parseInt(event.target.value));
}
private checkValue(item) {
if (item > this.maxValue) {
return this.maxValue;
}
else if (else < this.minValue) {
return this.maxValue;
}
return item;
}