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

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)
});
} );

Related

CKEditor5: How to create model element value from data attribute?

I have legacy html data that I'm trying to edit with CKEditor5. The data format is:
<my-data-element url="https://something.com/more/stuff"></my-data-element>
The desired model format is
<my-model-element>
https://something.com/more/stuff
</my-model-element>
where the url attribute in the data is now the text of the model element. my-model-element is an editable widget so the user can easily modify the existing URL, copy/paste/etc. When the model is convert to data, the text in my_model-element should be converted to the url value for my-data-element. Reading the value of the url attribute is relatively easy, but I can't figure out how to set the text of the my-model-element. While this looks similar to a link, it's not a link. I considered borrowing from the link editing code, but that's a lot of code and this should be a root level object.
For data down casting, extracting the value of the element to set as the url is easy. The code below leaves the text of my-model-element in my-data-element but I can deal with that for now. It also results in my-data-element having the attribute undefined="undefined", for some reason, but I can also live with that.
schema.register( 'my-model-element', {
isObject: true,
allowWhere: '$block',
allowAttributes: ['url'],
allowContentOf: '$block'
} );
conversion.for( 'dataDowncast' ).elementToElement( {
model: 'myElement',
view: ( modelItem, {writer: viewWriter } ) => {
const data = modelItem.getChild(0).data;
const elem = viewWriter.createContainerElement (
'my-data-element', { url: data }
);
return elem;
}
} );
conversion.for( 'dataDowncast' ).attributeToAttribute( {
model: 'url',
// view has to be a function or the url doesn't get updated
view: () => 'url',
});
For up casting I can get the url from my-data-element, but have not been successful setting the text of my-model-element. Instead, the text value of my-model-element remains empty.
conversion.for( 'upcast' ).elementToElement( {
model: ( viewElement, {writer: modelWriter }) => {
// Pulling the URL works
const url = viewElement.getAttribute('url');
// But creating the child of the new element doesn't
const text = modelWriter.createText(`${url} DOESNT WORK`);
const elem = modelWriter.createElement('my-model-element', {}, text);
return elem;
},
view: {
name: 'my-data-element',
}
} );
I've read the majority of the CKEditor5 documentation on up and down casting, and the tutorials on block, inline, and data driven widgets.

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

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;
}

How to override 'data-testid' in the 'findByTestId function from Cypress Testing Library

Most of my existing codebase uses a 'id' only in few places 'data-testId' attribute present.
tried this code
import { configure } from '#testing-library/cypress';
configure({ testIdAttribute: ['data-testId','id'] });
But, still its not working.
Is there any way to use 'id' value in any of the testing-library functions.
My HTML code is something like:
<div class="some random class name" id="userprofile-open" role="button">SB</div>
I want click that element with this code:
cy.findByTestId("userprofile-open", { timeout: 120000 }).click();
I don't think you can configure testing-library with an array of ids, ref API configuration,
import { configure } from '#testing-library/cypress'
configure({ testIdAttribute: 'id' })
But even this fails. Instead you have to use the Cypress command to change the attribute name (only one name is allowed).
cy.configureCypressTestingLibrary({ testIdAttribute: 'id' })
To use either/or attribute name you can change the attribute name on the fly, wrapping it in a custom command (based on Custom Queries)
Cypress.Commands.add('findByTestIdOrId', (idToFind) => {
let result;
const { queryHelpers } = require('#testing-library/dom');
let queryAllByTestId = queryHelpers.queryAllByAttribute.bind(null, 'data-testId');
result = queryAllByTestId(Cypress.$('body')[0], idToFind)
if (result.length) return result;
queryAllByTestId = queryHelpers.queryAllByAttribute.bind(null, 'id');
result = queryAllByTestId(Cypress.$('body')[0], idToFind);
if (result.length) return result;
throw `Unable to find an element by: [data-test-id="${idToFind}"] or [id="${idToFind}"]`
})
cy.findByTestIdOrId('my-id')
.should('have.attr', 'id', 'my-id')
// passes and logs "expected <div#my-id> to have attribute id with the value my-id"
Note this custom command works only for synchronous DOM.
If you need to have Cypress retry and search for either/or attribute, don't use testing-library in the custom command.
Instead use Cypress .should() to enable retry
Cypress.Commands.add('findByTestIdOrId', (selector, idToFind) => {
cy.get(selector)
.should('satisfy', $els => {
const attrs = [...$els].reduce((acc, el) => {
const id = el.id || el.getAttribute('data-test-id') // either/or attribute
if (id) {
acc.push(id)
}
return acc
}, [])
return attrs.some(attr => attr === idToFind); // retries when false
})
.first(); // may be more than one
})
cy.findByTestIdOrId('div', 'my-id')
.should('have.attr', 'id', 'my-id')
// passes and logs "expected <div#my-id> to have attribute id with the value my-id"
The usual cypress way - which has an inherent check on the element visibility and existence as well as included retries for a period of time is using cy.get()
If you want to select element using property like data-id you need this sintax: cy.get('[propertyName="propertyValue"]')
If you want select an element by CSS selector you just pass CSS selector like this:
cy.get('#id')

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 properly update nested data in redux

This is my current object which i need to update:
[
{ id: q1,
answers:[
{ id: a1,
answered: false
},
...
]
},
...
]
I can't figure out how to update this object and set for example answered = true.
Is there any better way saving this kind of object? I tried to use the update addon from React but can't get it to work properly.
You can update the answers list this way, in your reducer:
function update(state, action) {
// assuming you are passing an id of the item to be updated via action.itemId
let obj = state.whatever_list.filter(item => item.id === action.itemId)[0]
//assuming you are passing an id of the answer to be updated via action.answerId
//also, assuming action.payload contains {answered: true}
let answers = obj.answers.map(answer => answer.id === action.answerId ?
Object.assign({}, answer, action.payload) : answer)
obj = Object.assign({}, obj, {answers: answers})
return {
whatever_list: state.whatever_list.map(item => item.id == action.itemId? Object.assign({}, item, obj) : item)
}
}
Here is what your action might look like:
function updateAnswer(itemId, answerId, payload) {
return {
type: UPDATE_ANSWER,
itemId: itemId,
answerId: answerId,
payload: payload
}
}
In your react component class, assuming there is an event handler for monitoring whether if a question is answered:
export default class Whatever extends React.Component {
...
// assuming your props contains itemId and answerId
handleAnswered = (e) => {
this.props.dispatch(updateAnswer(this.props.itemId, this.props.answerId, {answered: true}))
}
...
}
So basically what happens is this:
Your event handler calls the action and pass the updated data to it
When your action is called, it returns the updated data along with a type parameter
When your reducer sees the type parameter, the corresponding handler will be triggered (the first piece of the code above)
The reducer will pull out the existing data from the list, replace the old data with the new one, and then return a list containing the new data
You can create a sub-reducer for the answers key. Look at this example:
https://github.com/rackt/redux/blob/master/examples/async/reducers/index.js
You could use dot-prop-immutable and an update would be as simple as:
return dotProp.set(state, 'quiz.0.answers.0.answered', true);

Resources