I am trying to define a dataModel something like this:
var MyDataModel = kendo.data.Model.define({
id: "id",
fields: {
name: {
type: "string"
},
myListOfThings: {
defaultValue: []
},
numberOfThings: {
type: "number",
}
}
});
What I want though is something more. I was wondering if there is a way to define "derived properties" in the model type. More specifically I want numberOfThings to always compute to the length of the array myListOfThings.
That is I want the ability create the object like:
var o = new MyDataModel({ name: "a name", myListOfThings: ['a','b','c']});
And then having a kendo widget with a column bound to numberOfThings displaying 3.
Do you know how to accomplish this?
Edit:
Thanks Atanas, your answer works.
I was able to edit the jsbin code to a declarative binding form that works for me.
I'm not quite there yet thought since in my solution code I am getting a javscript error at line 269 in kendo.all.js that reads
return new Function(argumentName, functionBody);
The error message is
"ReferenceError: count is not defined"
I'm trying to find where my code differs from the jsbin code. .. but if you have an idea of what might be the problem I am all ears. If not, no worries, thanks for the answer.
Use a function which will return the number of items. Here is an example: http://jsbin.com/ategub/1/edit
Related
I want to add content to a repeatable component in my beforeUpdate hook. (adding a changed slug to a “previous slugs” list)
in v3, I could just push new data on the component array and it would save.
in v4, it doesn’t work like that. Component data now holds __pivot: and such. I do not know how to add new data to this. I’ve tried adding a component with the entityService first, and adding that result to the array. It seemed to work, but it has strange behavior that the next saves puts in two entries. I feel like there should be an easier way to go about this.
It seems like the way to go about this is to create the pivot manually:
// create an entry for the component
const newRedirect = await strapi.entityService.create('redirects.redirect', {
data: {
from: oldData.slug,
},
});
// add the component to this model entry
data.redirects = [...data.redirects, {
id: newRedirect.id,
__pivot: { field: 'redirects', component_type: 'redirects.redirect' },
}];
But this feels pretty hacky. If I change the components name or the field key, this will break. I'd rather have a Strapi core way of doing this
the way strapi currently handles components is by providing full components array, so in case you want to inject something, you have to read components first and then apply full update, if it makes it clear.
Update
So after few hours of searching, had to do few hours of trail and error, however here is the solution, using knex:
module.exports = {
async beforeUpdate(event) {
// get previous slug
const { slug: previousSlug } = await strapi.db
.query("api::test.test")
.findOne({ where: event.params.where });
// create component
const [component] = await strapi.db
// this name of components table in database
.connection("components_components_previous_slugs")
.insert({ slug: previousSlug })
.returning("id");
// append component to event
event.params.data.previousSlugs = [
...event.params.data.previousSlugs,
{
id: component.id,
// the pivot, you have to copy manually
// 'field' is the name of the components property
// 'component_type' is internal name of component
__pivot: {
field: "previousSlugs",
component_type: "components.previous-slugs",
},
},
];
},
};
So, seems there is no service, or something exposed in strapi to create component for you.
The stuff that also required to be noted, on my first attempt i try to create relation manually in tests_components table, made for me after i added a repeatable component, to content-type, but after an hour more i found out that is WRONG and should not be done, seems strapi does that under the hood and modifying that table actually breaks logic...
so if there is more explanation needed, ping me here...
result:
You can update, create and delete component data that is attached to a record with Query Engine API, which is provided by Strapi.
To modify component data you just need the ID.
const { data } = event.params;
const newData = {
field1: value1,
etc...
};
await strapi.query('componentGroup.component').update({
where: { id: data.myField.id },
data: newData
})
When you have a component field that equals null you need to create that component and point to it.
const tempdata = await strapi.query('componentGroup.component').create(
{ data: newData }
);
data.myField = {
id: tempdata.id,
__pivot: {
field: 'myField',
component_type: 'componentGroup.component'
}
}
Se the Strapi forum for more information.
I have a list of checkboxes, and I need to be able to select them ( check them) and I should also be able to filter them and check them while they are filtered. I am able to select the item, filter the item, but as soon as I filter them and then check any value it unchecks the previously selected value. I know the reason why it unchecks because every time user checks/unchecks the value I start with the original checkbox value set. So when I check/uncheck a value in filtered set, it starts again with default set where checkbox value is set to false.
Any suggestions on how to make it work? That is works seamlessly with filtering and checking/unchecking the values.
It looks like a really simple issue but i am stuck with it from past few days. Replicated the issue here. Please take a look. Any suggestions are appreciated .
https://stackblitz.com/edit/angular-wmyxtd
variables = [
{
"label": "Phone",
"value": "phone",
checked: false
},
{
"label": "Machine Id",
"value": "machine_id",
checked: false
},
{
"label": "Address",
"value": "address",
checked: false
},
{
"label": "Store",
"value": "store",
checked: false
},
{
"label": "Email",
"value": "email",
checked: false
},
{
"label": "Name",
"value": "name",
checked: false
},
{
"label": "Credit Card",
"value": "credit_Card",
checked: false
},
{
"label": "Bank Account",
"value": "bank_account",
checked: false
}
]
variables$ = of(this.variables).pipe(shareReplay(1),delay(1000));
filteredFlexibleVariables$ = combineLatest(this.variables$, this.filterBy$).pipe(
map(([variables, filterBy]) => {
return variables.filter((item) => item.value.indexOf(filterBy) !== -1);
})
);
toggleStateSelectedVariables$ = combineLatest(
this.variables$,
this.toggleFlexibleVariableBy$
).pipe(
map(([availableVariables, clickedVariable]) => {
const clickedVariableValues = clickedVariable.map((item) => item.value);
if (clickedVariable.length) {
// If condition just to save availableVariables iteration for no reason if there is no clicked variables
return availableVariables.map((variable) => {
const checked = clickedVariableValues.indexOf(variable.value) !== -1;
return {
...variable,
checked
};
});
}
return availableVariables;
}),
);
flexibleVariables$ = merge(
this.filteredFlexibleVariables$,
this.toggleStateSelectedVariables$
);
I'd take a different approach. As you're trying to edit a group of values, this makes me think that using a form would be a good idea.
I'm one of the authors of ngx-sub-form and I'll explain the approach I would take using this library to (hopefully?) simplify the workflow and encapsulate the edition of the data then just be warned whenever the form value changes.
Before we start, here's a stackblitz you can play with to see my solution:
https://stackblitz.com/edit/angular-nhnmcm
First, I'd like to point out that the filter should not be tight to the data themselves. It should just show or hide data in the view based on the filter. For that, we can use a pipe:
filter-item.pipe.ts
#Pipe({
name: "filterItem"
})
export class FilterItemPipe implements PipeTransform {
public transform(item: Item, searchFilter: string): Item {
return item.value.includes(searchFilter);
}
}
And before I forget, here's what an "Item" looks like:
export interface Item {
label: string;
value: string;
checked: boolean;
}
Now, within our main component we don't want to be aware of how the data are being displayed nor visualized. The only thing which matters is:
Where do we get our data from?
When do we update them?
app.component.ts
#Component({
selector: "my-app",
templateUrl: "./app.component.html",
styleUrls: ["./app.component.css"]
})
export class AppComponent {
public items$: Observable<Item[]> = this.itemsService.items$;
public search: string = "";
constructor(private itemsService: ItemsService) {}
public itemsUpdated(items) {
this.itemsService.updateItems(items);
}
}
Then, from the view of the main component, we should have 2 components:
One to tell us what's the value of the filter
One to display the list of items and tell us when it's been updated
app.component.html
<app-items-filter (search)="search = $event"></app-items-filter>
<app-items-form
[items]="items$ | async"
(itemsUpdated)="itemsUpdated($event)"
[searchFilter]="search"
></app-items-form>
So far, it doesn't look too bad, does it?
But how should we build the app-items-form which seems to be holding all the magic?
items-form.component.ts
interface ItemsForm {
items: Item[];
}
#Component({
selector: "app-items-form",
templateUrl: "./items-form.component.html",
styleUrls: ["./items-form.component.css"]
})
export class ItemsFormComponent extends NgxAutomaticRootFormComponent<
Item[],
ItemsForm
> {
#DataInput()
#Input("items")
public dataInput: Item | null | undefined;
#Output("itemsUpdated")
public dataOutput: EventEmitter<Item> = new EventEmitter();
#Input()
searchFilter: string;
protected getFormControls(): Controls<ItemsForm> {
return {
items: new FormArray([])
};
}
protected transformToFormGroup(obj: Item[]): ItemsForm {
return {
items: obj
};
}
public getDefaultValues(): Partial<ItemsForm> {
return {
items: []
};
}
protected transformFromFormGroup(formValue: ItemsForm): Item[] | null {
return formValue.items;
}
}
It may look like a lot is going on there if you haven't discovered ngx-sub-form yet. So let me walk you through it.
We use a top level class called NgxAutomaticRootFormComponent. That class will ask us to implement the form which is going to hold our data and will automagically bind the form to our input and give us the updated value as an output every time the form is being updated.
It might be slightly easier but as it's an array we need to wrap the array into an object as ngx-sub-form only wants to deal with a FormGroup internally. But that formGroup can contain a FormArray (which is what we just did there).
Now, let's take a look to the view:
items-form.component.html
<ng-container [formGroup]="formGroup">
<ng-container [formArrayName]="formControlNames.items">
<app-item-ctrl [hidden]="!(itemCtrl.value | filterItem:searchFilter)"
*ngFor="let itemCtrl of formGroupControls.items.controls" [formControl]="itemCtrl">
</app-item-ctrl>
</ng-container>
</ng-container>
Few important things to notice:
- ngx-sub-form gives us access to a formGroup property that we can use directly without having to declare it ourselves
- as we're holding a list of values, we use the formArrayName directive to work with the FormArray
- we use the hidden directive provided natively by Angular to hide the controls which are not included by our filter
- last, we delegate the display/edit of the item to a sub component called app-item-ctrl
This is the "magic" part of ngx-sub-form. It's really easy to break down a form into sub form and also at the top level abstract how it's been displayed/edited.
Final part, let's take a look into the sub component:
item-ctrl.component.ts
#Component({
selector: "app-item-ctrl",
templateUrl: "./item-ctrl.component.html",
styleUrls: ["./item-ctrl.component.css"],
providers: subformComponentProviders(ItemCtrlComponent)
})
export class ItemCtrlComponent extends NgxSubFormComponent<Item> {
protected getFormControls(): Controls<Item> {
return {
label: new FormControl(),
value: new FormControl(),
checked: new FormControl()
};
}
}
Things to notice:
providers: subformComponentProviders(ItemCtrlComponent): behind the scenes, ngx-sub-form is a simple wrapper to deal with a ControlValueAccessor in an easier way and with a lot less boilerplate. Hence, we need to register the DI tokens required by a ControlValueAccessor through that simple function where you just pass the class of the current component
getFormControls this is the method where we have to pass an object that'll be used to create our internal formGroup
And the corresponding view:
item-ctrl.component.html
<div [formGroup]="formGroup">
{{ formGroupValues.label }}
<input type="checkbox" [formControl]="formGroupControls.checked" />
</div>
Conclusion:
When you need to edit data, I'd recommend using a form. Whether you use ngx-sub-form or decide to use forms directly is up to you.
If I'm explaining how to use ngx-sub-form though, it's because we've invested quite a lot of time at work to come up with that library as we do manage a lot of forms and we needed to abstract the complexity somehow. It has greatly simplified our workflow and I believe the solution above is quite verbose (by the number of components used) but really simple too as we haven't had to deal with streams for ex. (Even though I do love streams but I don't think that in that case it's the best approach).
Hope it helps and here's the final stackblitz I've made: https://stackblitz.com/edit/angular-nhnmcm
Wow.. so after a lot of brainstorming and frustrating moments I was able to solve this. I was thinking it in a different way. What I ended up doing was to create a stream of toggled Values (select/unselect checkboxes) and then filter on those toggled values.
So in summary -
Stream 1 - Merged stream of original set of variables & clicked variable. Use scan operator to get the updated stream at any point
Stream 2 - Consume the above stream and apply filter on the above stream.
toggleStateSelectedVariables$ = merge(this.variables$, this.toggleFlexibleVariableBy$).pipe(
throttleTime(100),
scan((availableVariables: NzCheckBoxOptionInterface[], clickedVariableValue: NzCheckBoxOptionInterface) => {
return availableVariables.map((variable) => {
// If its clicked, toggle it. If its selected, make it unselected. If unselected, select it
const checked = variable.value === clickedVariableValue.value ? !variable.checked : variable.checked;
return {
...variable,
checked
};
});
}),
);
flexibleVariables$ = combineLatest(this.toggleStateSelectedVariables$, this.filterBy$).pipe(
map(([variables, filterBy]) => variables.filter((item) => item.value.indexOf(filterBy) !== -1);
));
Implementation available here - https://stackblitz.com/edit/angular-ymft6o
Hope this helps someone. If anyone has any better solutions, please chime in
Happy Learning,
Vatsal
value is ReinsDepositAmount
**
output
**
I have recently stocked with one of the application date sorting in the kendo UI grid.
In kendo grid, the column name is defined like this
Incoming value to ReinsDepositDate - Month,date,year format. 08-23-1991
Field name is ReinsDepositDate:
{
field: "ReinsDepositDate", type: "date", title: "Due Date",format: "{0:M/d/yyyy}", width: 100, filterable: {
cell: {
operator: "contains"
}
}
},
When Sorting the date, its sorting based on first values
1/12/1994
23/1/2015
13/1/1992
means while ascending I am getting
1/12/1994
13/1/1992
23/1/2015
So I have put the schema model
still, I am getting the same result.
schema: {
model: {
fields: {
ReinsDepositDate: { type: "date",format: "{0:dd/MM/yyyy}"}
}
}
},
I have seen lots of fiddle demos nothing works here why:
Refrences:
http://fiddle.jshell.net/NqrDS/light/
Kendo grid date column not formatting
Flow of Design:
Flow of design is by using angular Http service get the value from DB through API and assign the response to datasource in the kendo grid. when I doing a demo with JSON files, its working fine. But the same thing applies it here means not working. so I went to custom javascript to sort. columns:[$scope.grdPrmiumDepositCol, –
Custom javascript in kendo sortable attribute will do the trick.
working fine do this part.
{ field: "ReinsDepositDate", format: "{0:MM/dd/yyyy}",type:"date", sortable:{ compare: function (a, b) {
var c = new Date(a.ReinsDepositDate);
var d = new Date(b.ReinsDepositDate);
return c - d;
}`
}}],
My Question is why I do that because kendo is given the date format and when I tried the sample demo with transport read with JSON file working fine with kendo format. Still in confusion.
Based on the provided information, it is unclear if sorting is performed on the client, or on the server.
If sorting is done on the client by the Kendo UI DataSource, then the date values should be provided in the correct format, so that they are parsed to JavaScript Date objects by Kendo UI. There are multiple different formats that can be parsed, but dd-MM-yyyy is not one of them.
Here is an example, which demonstrates the above. You will notice the empty row, where the date has not been parsed.
http://dojo.telerik.com/UcEXO/2
Generally, it is recommended to serialize dates using commonly accepted standards:
https://stackoverflow.com/a/15952652/3086237
If sorting is performed on the server, then Kendo UI is unrelated to the problem and you should debug the server-side implementation.
You can try parsing the date from response.
http://docs.telerik.com/kendo-ui/api/javascript/data/datasource#configuration-schema.parse
schema: {
parse: function(response) {
for (var i = 0; i < response.length; i++) {
response[i].ReinsDepositDate = kendo.parseDate(response[i].ReinsDepositDate, "dd/MM/yyyy");
}
return response;
}
}
Hope this helps.
I have the following issues with my knockout model validations and not sure how to resolve them. Following is my model first of all, with the validation rules:
var Data = function (data) {
this.Val = data;
}
function ViewModel(item) {
var parse = JSON.parse(item.d);
var self = this;
this.Name = ko.observable(parse.Name);
this.UserType = ko.observable(parse.UserType);
this.ID = ko.observable(parse.ID).extend({ required: { params: true, message: "ID is required" }, decimal: { params: 2, message: "Should be decimal"} });
this.Username = ko.observable(parsed.Username).extend({ required: {
onlyIf: function () {
return self.UserType() > 1;
}
}
});
this.WeeklyData = ko.observableArray([]);
var records = $.map(parse.WeeklyData, function (data) { return new Data(data) });
this.WeeklyData(records);
this.WeeklyData2 = ko.observableArray([]);
var records = $.map(parse.WeeklyData2, function (data) { return new Data(data) });
this.WeeklyData2(records);
}
ko.extenders.numeric = function (target, precision) {
var result = ko.dependentObservable({
read: function () {
return target().toFixed(precision);
},
write: target
});
result.raw = target;
return result;
};
Here are my problems:
1) with the ID() observable, I want to restrict it to two decimal points, so I've created the validation extender 'numeric' but it's not working. Is there anything wrong with how I'm using it and how to correct it?
2) Also, if I want to restrict an observable to whole numbers, how can I do that?
3) when I define a rule with a condition, (i.e. Username()), how do I define a custom message for that? I was able to do it for default rules, but with the conditional rules, it's not working
4) I have two observable arrays WeeklyData1 and WeeklyData2 both of which contains Data() objects. I want to have separate min/max rules for these two, for example, min/max - 1,7 for WeeklyData1 and min/max - 1,150 for WeeklyData2. How can I get it done?
4) Right now my error messages appear right next to the data field, but I want all those to appear in a single validation summary, while displaying '*' against the field. I've been told to use Validation-bindings, but I'm not sure how to use it, can someone please give an example?
It's a lot of questions, I know, but I appreciate if someone could help.
Thanks in advance
Instead of diving in your code i have created a small-small demonstrations for your questions. Ok so here we go,
1) with the ID() observable, I want to restrict it to two decimal points.... and 2) Also, if I want to restrict an observable to whole numbers....
Your 1 and 2 question are pretty similar so i covered both of this in a single fiddle. Check this fiddle.
3) when I define a rule with a condition, (i.e. Username()), how do I define a custom message ....
You can use message property to set custom messages, Check this fiddle.
4) I have two observable arrays WeeklyData1 and WeeklyData2 both of which contains Data() objects
I am not clear which this question, what type of data both of these array contains and for what you want to set min/max rule ( array length or other ). So please clear this, than i will try to help on this.
5) Right now my error messages appear right next to the data field.....
This questions answer i already given in your how to? with knockout js validations question (Check update).
Let me know if it helps!
I have found plenty of posts about this, but none of them are solving my issue. My code right now:
#Html.ListBox("SelectedNewsletter", Model.Newsletters)
and
public MultiSelectList Newsletters
{
get
{
return new MultiSelectList(
new[]
{
// TODO: Fetch from your repository
new { Id = 1, Name = "item 1" },
new { Id = 2, Name = "item 2" },
new { Id = 3, Name = "item 3" },
},
"Id",
"Name"
);
// return new MultiSelectList(PromoNewsletter.All, "IdString", "Display");
}
}
As far as I can see, it's all hard coded now, and it still gives the same error. I want to do a ListboxFor, but I am trying to just get a listbox to work. I have replaced my int id with a string representation, based on advice I found elsewhere, but now I don't see what else I can do. It just plain is not working, even with all hard coded values and not binding to a property on my ViewModel. Where am I going wrong ?
The error is occurring because you have a property in the Model or ViewData/ViewBag with the name SelectedNewsletter.
Give a different name for the ListBox if you can't rename that property.
UPDATE:
After little more experimenting I figured out that the problem is you may be setting an integer value to the SelectedNewsletter that is somewhere in the ViewData/ViewBag or Model.
You can set the values that has to be selected in the ListBox as a string or array of strings to the SelectedNewsletter.
i.e SelectedNewsletter = "1";
or
SelectedNewsletter = new[] { "1", "3" };
You can also use strongly typed helper to make things easy,
#Html.ListBoxFor(m => m.SelectedNewsletter, Model.NewsLetters);