Storybook problem while migrating argument of type object from addon-knobs to addon-controls - arguments

I'm having some trouble migrating one thing from the old addon-knobs to the new controls. Let me explain, maybe it's not such difficult task but I'm blocked at the moment.
I'm using StencilJS to generate Web Components and I have a custom select component that accepts a options prop, this is an array of objects (the options of the select)
So, the story for this component in the previous version of Storybook looks something like this:
export const SelectWithArray = () => {
const selectElement = document.createElement('my-select');
selectElement.name = name;
selectElement.options = object('Options', options);
selectElement.disabled = boolean('Disabled', false);
selectElement.label = text('Label', 'Label');
return selectElement;
};
This works fine, the select component receives the options property correctly as an array of objects.
Now, migrating this to the new Storybook version without addon-knobs, the story is looking like this:
const TemplateWithArray: Story<ISelect> = (args) => {
return `
<my-select
label="${args.label}"
disabled="${args.disabled}"
options="${args.options}"
>
</my-select>
`;
};
export const SelectWithArray: Story<ISelect> = TemplateWithArray.bind({});
SelectWithArray.argTypes = {
options: {
name: 'Options',
control: { type: 'object' },
}
}
SelectWithArray.args = {
options: [
{ text: 'Option 1', value: 1 },
]
}
And with this new method, the component is not able to receive the property as expected.
I believe the problem is that now, the arguments is being set directly on the HTML (which would only be accepting strings) and before it was being set on the JS part, so you could set attributes other than strings.
Is there a way to achieve this? without having to send the arguments as a string.
Thanks a lot!!

One way I've discovered so far is to bind the object after the canvas has loaded via the .play function;
codeFullArgs.play = async () => {
const component = document.getElementsByTagName('your-components-tag')[0];
component.jobData = FullArgs.args.jobData;
}

Related

How to create Strapi component data in a lifecycle

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.

How to avoid attribute-operation-attribute-exists when calling writer.setAttribute() in CKEditor5?

I am developing a simple CKEditor5 plug-in. Part of the plug-in is a "Command" that executes like this:
execute(options) {
const contentItemUtils = this.editor.plugins.get('ContentItemUtils');
const contentItemElement = contentItemUtils.getClosestSelectedContentItemElement(this.editor.model.document.selection);
this.editor.model.change(writer => {
writer.setAttribute('width', options.width, contentItemElement);
});
}
The problem happens when I call writer.setAttribute. I always get an error like this:
CKEditorError: attribute-operation-attribute-exists {"node":{"attributes":{"contentId":"CORE08954D2EBB7042799E0A059DC90703DD","contentName":"Paris","contentType":"Destination","contentTypeDisplay":"Destination","contentViewing":"draft","categoryLayout":"overview","detailPageId":"","alignment":""},"name":"contentItem"},"key":"width"}
Read more: https://ckeditor.com/docs/ckeditor5/latest/framework/guides/support/error-codes.html#error-attribute-operation-attribute-exists
What I am trying to do -- set a model attribute to a new value -- seems fairly simple.
Is there a restriction about updating model attributes that already have values?
I ended up first removing the attribute and then adding it :
editor.model.change( writer => {
const element = selection.getSelectedElement();
writer.removeAttribute( 'format', element)
editor.model.change( writer => {
writer.setAttribute( 'format', 'date', element)
});
} );

Accessing reactive properties with vee-validate 4

I am getting my head around Vee-Validate next (v4) and how I might incorporate it in a Vue 3 project without loosing Vue's reactivity (i.e. not relying on the values simply being passed to the Form submit event).
By way of example, if I were making a hypothetical component which has autocomplete functionality, and sent a get request to the server once 3 letters had been typed, but for the input itself to be valid it required 8 letters, how would I get the value associated with the input?
using plain Vue, with pseudo-code something like:
defineComponent({
setup () {
const myVal = ref('')
const options = ref([])
watchEffect(() => if (myVal.value.length > 3) {
axios.get(...).then(serverVals => options.value = serverVals))
})
return { myVal, options }
how would I achieve this with vee-validate 4.x?
defineComponent({
setup () {
const schema = yup.object({ myVal: yup.string().required().min(8) })
// ???? what now to watch myVal
please note this is not about autocomplete - a range slider where I wanted a server call when the value was greater than 10 but a validation message if greater than 90 would also suffice as an example.
You could employ useField here to get a reactive value that's automatically watched.
const { value: myVal, errorMessage } = useField('myVal', undefined, {
initialValue: ''
});
const options = ref([])
watchEffect(() => if (myVal.value.length > 3) {
axios.get(...).then(serverVals => options.value = serverVals))
})
return { myVal, options }
Documentation has an example of using useField:
https://vee-validate.logaretm.com/v4/guide/composition-api#usefield()
Note that you don't have to use useForm, if you are using <Form> component and passing schema to it then that should work just fine.

How to get multiple properties from objects in JXA?

Is there a way in JXA to get multiple properties from multiple objects with a single call?
For example, I want to get name and enabled property from menu items which can be done for each individual property as follows:
Application("System Events").processes.byName('Finder').menuBars[0].menuBarItems.name()
Application("System Events").processes.byName('Finder').menuBars[0].menuBarItems.enabled()
but is it possible to get them with a single function call? Something like:
Application("System Events").processes.byName('Finder').menuBars[0].menuBarItems.select('name', 'enabled')
I know, that I can iterate through the menuBarItems and collect properties from .properties() method, but this approach is too slow, that's why I'm looking for other options.
UPDATE
I'm looking for better performance, not for nicer syntax, i.e. I want properties to be retrieved in a single call to System Events.
I'd probably do it like this:
sys = Application('com.apple.systemevents');
FinderProc = sys.processes['Finder'];
FinderMenuBarItems = FinderProc.menuBars[0].menuBarItems();
Array.from(FinderMenuBarItems,x=>[x.name(),x.enabled()]);
By first converting the object to an array, this allows one to map each element and retrieve the desired properties for all in one go. The code is split over several lines for ease of reading.
EDIT: added on 2019-07-27
Following on from your comment regarding Objective-C implementation, I had a bit of time today to write a JSObjc script. It does the same thing as the vanilla JXA version above, and, yes, it clearly makes multiple function calls, which is necessary. But it's performing these functions at a lower level than System Events (which isn't involved at all here), so hopefully you'll find it more performant.
ObjC.import('ApplicationServices');
ObjC.import('CoreFoundation');
ObjC.import('Foundation');
ObjC.import('AppKit');
var err = {
'-25211':'APIDisabled',
'-25206':'ActionUnsupported',
'-25205':'AttributeUnsupported',
'-25204':'CannotComplete',
'-25200':'Failure',
'-25201':'IllegalArgument',
'-25202':'InvalidUIElement',
'-25203':'InvalidUIElementObserver',
'-25212':'NoValue',
'-25214':'NotEnoughPrecision',
'-25208':'NotImplemented',
'-25209':'NotificationAlreadyRegistered',
'-25210':'NotificationNotRegistered',
'-25207':'NotificationUnsupported',
'-25213':'ParameterizedAttributeUnsupported',
'0':'Success'
};
var unwrap = ObjC.deepUnwrap.bind(ObjC);
var bind = ObjC.bindFunction.bind(ObjC);
bind('CFMakeCollectable', [ 'id', [ 'void *' ] ]);
Ref.prototype.nsObject = function() {
return unwrap($.CFMakeCollectable(this[0]));
}
function getAttrValue(AXUIElement, AXAttrName) {
var e;
var _AXAttrValue = Ref();
e = $.AXUIElementCopyAttributeValue(AXUIElement,
AXAttrName,
_AXAttrValue);
if (err[e]!='Success') return err[e];
return _AXAttrValue.nsObject();
}
function getAttrValues(AXUIElement, AXAttrNames){
var e;
var _AXAttrValues = Ref();
e = $.AXUIElementCopyMultipleAttributeValues(AXUIElement,
AXAttrNames,
0,
_AXAttrValues);
if (err[e]!='Success') return err[e];
return _AXAttrValues.nsObject();
}
function getAttrNames(AXUIElement) {
var e;
var _AXAttrNames = Ref();
e = $.AXUIElementCopyAttributeNames(AXUIElement, _AXAttrNames);
if (err[e]!='Success') return err[e];
return _AXAttrNames.nsObject();
}
(() => {
const pid_1 = $.NSWorkspace.sharedWorkspace
.frontmostApplication
.processIdentifier;
const appElement = $.AXUIElementCreateApplication(pid_1);
const menuBar = getAttrValue(appElement,"AXMenuBar");
const menuBarItems = getAttrValue(menuBar, "AXChildren");
return menuBarItems.map(x => {
return getAttrValues(x, ["AXTitle", "AXEnabled"]);
});
})();

Backbone.js + MVC3. Nested collection doesn't get populated

I have a backbone collection on the client.
Model of the collection has some properties along with another collection
When I do fetch() my action method on the server returns some data, collection gets populated, all the properties too, except that nested collection.
What could be the reason?
var Job = Backbone.Model.extend();
var Jobs = Backbone.Collection.extend({model: Job})
var Foo = Backbone.Model.extend({
initialize:function(){
this.jobs = new Jobs();
}})
var FooCollection = Backbone.Collection.extend({model: Foo})
var fooCol = new FooCollection()
fooCol.fetch();
fooCol.first().get('name') // => returns name
fooCol.first().jobs.toJSON() // returns nothing
// although this will
fooCol.first().get('jobs') //it will return an array
So somehow nested Backbone collection becomes just a regular property (Array)
OK - with your extra information, I can give you an answer.
First - "get" doesn't get a property off of the model. It gets a property off of the model's attributes property. So, the attributes probably look like:
{
name: 'blah',
jobs: [{name: 'job1'}, {name: 'job2'}]
}
Backbone doesn't automagically transform arrays into collections and models, and simply setting this.jobs isn't going to work. What you need to do is a little more complex.
var Foo = Backbone.Model.extend({
initialize:function(){
this.jobs = new Jobs(this.attributes.jobs));
}
});
This will set your 'jobs' property to a new jobs object with the data that was sent over for the jobs. But, alas, it won't automatically fire events on the Jobs collection, nor will it allow you to use helpers like this.get('jobs').each(fn); - you'll only be able to use it as Foo.jobs.each(fn).
In order for you to use the attribute as an actual collection, you'll have to do a lot more complicated things.
var Foo = Backbone.Model.extend({
initialize:function(){
this.createJobs(this.attributes.jobs);
},
toJSON: function () {
var json = Backbone.Model.prototype.toJSON.apply(this);
json.jobs = this.get('jobs').toJSON();
return json;
},
set: function (key, val) {
var attributes;
if(!_.isObject(key)) {
attributes = {}; attributes[key] = val;
} else {
attributes = key;
}
safeAttributes = _.omit(attributes, 'jobs');
Backbone.Model.prototype.set.call(this, safeAttributes);
if(attributes.jobs) { this.get('jobs').reset(attributes.jobs); }
},
clear: function () {
if(this.get('jobs') && this.get('jobs').destroy) {
this.get('jobs').off();
this.get('jobs').destroy();
}
Backbone.Model.prototype.clear.apply(this);
this.createJobs();
},
createJobs: function (jobsArray) {
var jobsCollection = new Jobs(jobsArray);
jobsCollection.on('change', function () {this.trigger('change'); }, this);
this.set('jobs', jobsCollection);
}
});
Note that this is completely untested, but hopefully it shows some of the way you'd do this.

Resources