Refreshing a CKEditor5 widget upon model changes - widget

I have a custom schema element with a text attribute node that renders as a widget for the editingDowncast:
conversion.for( 'editingDowncast' )
.add( downcastElementToElement({
model: 'myModelElement',
view: (modelElement, viewWriter) => {
return createMyModelWidget( modelElement, viewWriter, 'Label' );
}
} )
);
function createMyModelWidget( modelElement, writer, label ) {
const content = modelElement.getAttribute('content')
const placeholder = writer.createText( content );
const container = writer.createContainerElement( 'div', { class: 'my-model-element--widget' } );
writer.insert( ViewPosition.createAt( container ), placeholder );
return toWidget( container, writer, label );
}
Through some external event (e.g., result from an external modal that configures the widget), the attribute is updated using model.change.
model.change(writer => {
writer.setAttribute( 'attribute', 'whatever', widget );
writer.setAttribute( 'content', 'new content', widget );
});
Now since its attribute changed, I would expect the widget to be re-rendered, however that is not the case. How can I manually trigger a refresh to ensure it is up-to-date?

The refreshing of an view widget on model changes is handled by downcast conversion. I can see that you want convert a model attribute to a text contents of a <div>. I assume that the "attribute" attribute is a <div>s element attribute (like data-attribute). In such case you'll need set of converters:
Upcast
a single element to element upcast converter that will create myModelElement model element instance.
Downcast
an element to element downcast converter that will create div for newly inserted myModelElment into a model
a custom attribute converter that will update div contents on attribute content changes
A two-way converter that will cover simple attribute to attribute conversion (both upcast & downcast).
ps.: I've used "downcast" instead of "editingDowncast" to not define the downcast conversion for "editingDowncast" and "dataDowncast" separately.
// Required imports:
// import { toWidget } from '#ckeditor/ckeditor5-widget/src/utils';
// import { downcastElementToElement } from '#ckeditor/ckeditor5-engine/src/conversion/downcast-converters';
// import { upcastElementToElement } from '#ckeditor/ckeditor5-engine/src/conversion/upcast-converters';
// import ViewPosition from '#ckeditor/ckeditor5-engine/src/view/position';
// import ViewRange from '#ckeditor/ckeditor5-engine/src/view/range';
const conversion = editor.conversion;
const model = editor.model;
const schema = model.schema;
// Define model element and allow attributes on it.
schema.register( 'myModelElement', {
allowWhere: '$block',
allowAttributes: [ 'content', 'attribute' ],
isObject: true
} );
// Simple attribute to attribute converter two-way converter
// - for upcast it will read "data-attribute" property of div and put it into model's "attribute" attribute
// - for downcast it will update "data-attribute" on changes in "attribute".
conversion.attributeToAttribute( {
model: {
name: 'myModelElement',
key: 'attribute'
},
view: {
key: 'data-attribute'
}
} );
// Define upcast conversion:
conversion.for( 'upcast' ).add( upcastElementToElement( {
model: ( viewElement, modelWriter ) => {
const firstChild = viewElement.getChild( 0 );
const text = firstChild.is( 'text' ) ? firstChild.data : '(empty)';
return modelWriter.createElement( 'myModelElement', { content: text } );
},
view: {
name: 'div',
classes: 'my-model-element--widget'
}
} ) );
// Define downcast conversion:
conversion.for( 'downcast' )
.add( downcastElementToElement( {
model: 'myModelElement',
view: ( modelElement, viewWriter ) => {
return createMyModelWidget( modelElement, viewWriter, 'Label' );
}
} ) )
// Special conversion of attribute "content":
.add( dispatcher => dispatcher.on( 'attribute:content', ( evt, data, conversionApi ) => {
const myModelElement = data.item;
// Mark element as consumed by conversion.
conversionApi.consumable.consume( data.item, evt.name );
// Get mapped view element to update.
const viewElement = conversionApi.mapper.toViewElement( myModelElement );
// Remove current <div> element contents.
conversionApi.writer.remove( ViewRange.createOn( viewElement.getChild( 0 ) ) );
// Set current content
setContent( conversionApi.writer, data.attributeNewValue, viewElement );
} ) );
function createMyModelWidget( modelElement, writer, label ) {
const content = modelElement.getAttribute( 'content' );
const container = writer.createContainerElement( 'div', { class: 'my-model-element--widget' } );
setContent( writer, content, container );
return toWidget( container, writer, label );
}
function setContent( writer, content, container ) {
const placeholder = writer.createText( content || '' );
writer.insert( ViewPosition.createAt( container ), placeholder );
}

Related

React Beautiful DnD, multiple columns inside single droppable

I am trying to have a grid column layout, (2 columns) inside a single droppable container. The project is for an online menu where you can create a menu item, which goes into a droppable container, then you can drag that onto the menu that will be displayed to the user. So there is currently two columns. However the style of the menu demands two columns. Currently I am assigning different classNames to the mapped columns so I can make one of them grid but its pretty messy. Maybe there is a way I can hardcode the droppable instead of map them and run the map on the lists themselves inside each of the hardcoded droppables? Sorry if this is confusing, it sure is for me.
'results' is API data that is initially mapped into savedItems array where newly created menu items will go. Later on menuItems array will pull from the database as well. Right now just trying to have better styling control over the different droppables.
you can see where im assigning different classNames to the droppable during the mapping and its really not a reliable option.
//drag and drop states
const [state, setState] = useState({
menuItems: {
title: "menuItems",
items: []
},
savedItems: {
title: "savedItems",
items: results
}
})
useEffect(() => {
setState({ ...state, savedItems: { ...state.savedItems, items: results } })
}, [results])
// console.log("state", state)
console.log("dummy data", dummyArry)
// updating title graphql mutation
const [elementId, setElementId] = useState(" ");
const updateTitle = async () => {
//api data
const data = await fetch(`http://localhost:8081/graphql`, {
method: 'POST',
body: JSON.stringify({
query: `
mutation {
updateMenu(menuInput: {_id: ${JSON.stringify(elementId)},title: ${JSON.stringify(inputValue)}}){
title
}
}
`
}),
headers: {
'Content-Type': 'application/json'
}
})
//convert api data to json
const json = await data.json();
}
//drag end function
const handleDragEnd = (data) => {
console.log("from", data.source)
console.log("to", data.destination)
if (!data.destination) {
// console.log("not dropped in droppable")
return
}
if (data.destination.index === data.source.index && data.destination.droppableId === data.source.droppableId) {
// console.log("dropped in same place")
return
}
//create copy of item before removing from state
const itemCopy = { ...state[data.source.droppableId].items[data.source.index] }
setState(prev => {
prev = { ...prev }
//remove from previous items array
prev[data.source.droppableId].items.splice(data.source.index, 1)
//adding new item to array
prev[data.destination.droppableId].items.splice(data.destination.index, 0, itemCopy)
return prev
})
}
const columnClass = [
"menuItems-column",
"savedItems-column"
]
let num = 0
return (
<>
<div className='app'>
{results && <DragDropContext onDragEnd={handleDragEnd}>
{_.map(state, (data, key) => {
return (
<div key={key} className='column'>
<h3>{data.title}</h3>
<Droppable droppableId={key}>
{(provided, snapshot) => {
return (
<div
ref={provided.innerRef}
{...provided.droppableProps}
className={columnClass[num]}
// className="droppable-col"
><span className='class-switch'>{num++}</span>
{data.items.map((el, index) => {
return (
<Draggable key={el._id} index={index} draggableId={el._id}>
{(provided) => {
return (
<div className='element-container'
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}
>
<div contentEditable="true">
{el.title}
</div>
</div>
)
}}
</Draggable>
)
})}
{provided.placeholder}
</div>
)
}}
</Droppable>
</div>
)
})}
</DragDropContext>}
</div>
</>
)
}

React erroring because of different, mystery object

I'm trying to render some JSON and the error I get references some fields that don't exist in my JSON structure. The fields are getting logged to the console properly.
Which object is this referring to, and how do I fix it?
Error: Objects are not valid as a React child (found: object with keys {_events, _eventsCount, _maxListeners, uri, callback, readable, writable, _qs, _auth, _oauth, _multipart, _redirect, _tunnel, headers, setHeader, hasHeader, getHeader, removeHeader, method, localAddress, pool, dests, __isRequestRequest, _callback, proxy, tunnel, setHost, originalCookieHeader, _disableCookies, _jar, port, host, path, httpModule, agentClass, agent}). If you meant to render a collection of children, use an array instead.
Postlist:
class PostList extends React.Component {
render() {
return (
request('http://194.5.192.153:3044/api/posts/', function (error,response,body) {
let items = JSON.parse(body).items;
for(let i=0; i<items.length; i++) {
console.log(items[i].author,items[i].desc,items[i].updatedAt,items[i].title);
return (
<Post author={items[i].author} desc={items[i].desc} image={items[i].image} title={items[i].title} createdAt={items[i].createdAt} updatedAt={items[i].updatedAt} />
);
}
})
)
}
}
Post:
const Post = (props) => {
return (
<>
<img src={props.image} alt="" />
<h1>{props.title}</h1>
<h2>by {props.author}</h2>
<div>Created at {props.createdAt}</div>
<div>Updated at {props.updatedAt}</div>
<div>{props.desc}</div>
</>
);
}
export default Post;
In your code, you are effectively trying to render the return value from request, which is certainly not what you want. Since request is asynchronous, the general pattern here is to set state in the callback and then map over that state in the render method.
class PostList extends React.Component {
state = { items: [] };
componentDidMount() {
request(
"http://194.5.192.153:3044/api/posts/",
(error, response, body) => {
const items = JSON.parse(body).items;
this.setState({ items });
}
);
}
render() {
return this.state.items.map((item) => (
<Post
key={item.title}
author={item.author}
desc={item.desc}
image={item.image}
title={item.title}
createdAt={item.createdAt}
updatedAt={item.updatedAt}
/>
));
}
}

React form with nested json and controlled components

I'm buidling a react form with reusable components.
I'd like to update my json so I can nest some data, but I'm getting a "controlled vs uncontrolled" error (again!) which I can't solve. I've been researching on it, but I'm probably lacking the right vocabulary to be efficient. Where or how should I define "pet"..?
Later, my goal is to be able to add several pets to one owner.
PetsForm
import React, { useState, useEffect } from 'react';
import Controls from './reusables/Controls';
import { useForm, Form } from './reusables/useForm';
const initialFieldValues = {
id: 0,
owner: '',
pet : [{
petName: '',
petBreed: ''
}]
}
export default function PetsForm(props) {
const {
values,
setValues,
errors,
setErrors,
handleInputChange,
resetForm
} = useForm(initialFieldValues, true, validate);
return (
<Paper>
<Box>
<Controls.Input
label="Owner"
name='owner'
placeholder="Owner"
value={values.owner = values.owner.toUpperCase()}
onChange={handleInputChange}
/>
<Controls.Input
label="PetName"
name='petName'
placeholder="Pet Name"
value={values.petName = values.petName.toUpperCase()}
onChange={handleInputChange}
/>
<Controls.Input
label="PetBreed"
name='petBreed'
placeholder="Pet Breed"
value={values.petBreed}
onChange={handleInputChange}
/>
</Box>
</Paper>
etc...
}
My useForm
export function useForm(initialFieldValues, validateOnChange=false, validate) {
const [values, setValues] = useState(initialFieldValues);
const [errors, setErrors] = useState({});
const handleInputChange = event => {
const { name, value } = event.target
setValues({
...values,
[name]: value,
})
if(validateOnChange)
validate({
[name]: value,
})
}
return {
values,
setValues,
errors,
setErrors,
handleInputChange,
};
}
You are getting the error because your values values.petName and values.petBreed is undefined. Because your shape of values is
const initialFieldValues = {
id: 0,
owner: '',
pet : [{
petName: '',
petBreed: ''
}]
}
So the petname and petBreed has to refered as values.pet.[0].petName and values.pet.[0].petBreed .
Setting the value of nested Object
You can use the lodash set to set the value of deeply nested object . All you need to do is to provide the name as dotted path values . For petname you need to provide the name attribute as pet.[0].petName
Now we can set the values of your value object using lodash set,
const handleInputChange = event => {
const { name, value } = event.target
// clone the values . you can use lodash cloneDeep for this
set(clonedValues, name, value);
setValues(clonedValues);
....
}
Now you can retrieve the deeply nested values as
<Controls.Input
label="PetBreed"
name='pet.[0].petBreed'
placeholder="Pet Breed"
value={values.pet.[0].petBreed}
onChange={handleInputChange}
/>

Mapping from JSON Get Request. Undefined

I am trying to connect to the USDA Food Central database using an API.
let uri = encodeURI(`https://api.nal.usda.gov/fdc/v1/foods/search?api_key=${MY_API_KEY}&query=${search}`)
I want to use the API to map certain fields.
class AddFoodItemList extends Component {
static contextType = AddFoodContext;
render() {
const listItems = this.context.FoodSearch.map((foods) =>
<FoodItem
key={foods.brandOwner}
brandOwner={foods.brandOwner}
fdcId={foods.fdcId}
/>
);
return (
<div id="AddFoodItemList">
{listItems}
</div>
);
}
}
export default AddFoodItemList;
The returned JSON is this screenshot attached:
Returned JSON
I am getting an error, TypeError: Cannot read property 'map' of undefined.
Why do you think this is the case? Any sort of help or suggestions are appreciated!
You are attempting to access a property FoodSearch on the value of your AddFoodContext provider. The error tells you that this property is undefined. If the object in your screenshot is the value of your context then you want to access the property foods instead. This is an array whose elements are objects with properties brandOwner and fdcId.
On your first render this data might now be loaded yet, so you should default to an empty array if foods is undefined.
It's honestly been a long time since I've used contexts in class components the way that you are doing it. The style of code is very dated. How about using the useContext hook to access the value?
const AddFoodItemList = () => {
const contextValue = useContext(AddFoodContext);
console.log(contextValue);
const listItems = (contextValue.foods || []).map((foods) => (
<FoodItem
key={foods.fdcId} // brandOwner isn't unique
brandOwner={foods.brandOwner}
fdcId={foods.fdcId}
/>
));
return <div id="AddFoodItemList">{listItems}</div>;
};
Here's a complete code to play with - Code Sandbox Link
const MY_API_KEY = "DEMO_KEY"; // can replace with your actual key
const getUri = (search) => `https://api.nal.usda.gov/fdc/v1/foods/search?api_key=${MY_API_KEY}&query=${encodeURIComponent(search)}`;
const AddFoodContext = createContext({});
const FoodItem = ({ brandOwner, fdcId }) => {
return (
<div>
<span>{fdcId}</span> - <span>{brandOwner}</span>
</div>
);
};
const AddFoodItemList = () => {
const contextValue = useContext(AddFoodContext);
console.log(contextValue);
const listItems = (contextValue.foods || []).map((foods) => (
<FoodItem
key={foods.fdcId} // brandOwner isn't unique
brandOwner={foods.brandOwner}
fdcId={foods.fdcId}
/>
));
return <div id="AddFoodItemList">{listItems}</div>;
};
export default () => {
const [data, setData] = useState({});
useEffect(() => {
fetch(getUri("cheese"))
.then((res) => res.json())
.then(setData)
.catch(console.error);
}, []);
return (
<AddFoodContext.Provider value={data}>
<AddFoodItemList />
</AddFoodContext.Provider>
);
};

How do I insert a value in a custom field in a table in Prestashop?

I added a custom field named "deldate" in "ps_orders" table, and I added a text box on "OPC" checkout page. Now when I click on order confirm button the value in the textbox should be saved in "deldate" field of "ps_orders" table.
The textbox is showing perfectly but in which files do I need to make changes to save the textbox value in the table?
(Theme is default one.)
class/order/order.php
class OrderCore extends ObjectModel
{
public $deldate;
}
And
public static $definition = array(
'fields' => array(
'deldate'=> array('type' => self::TYPE_STRING),
),
)
Shopping-cart.tpl
<div class="box">
<div class="required form-group">
<form method="post">
<label for="Fecha de entrega deseada">{l s='Desired delivery date' mod='deldate'}</label>
<input type="text" id="deldate" name="deldate" class="form-control" value="hello" />
</form>
</div>
</div>
Ok, I figured out the solution...
If you want to add some information to the order in the checkout process you have to save this informations elsewhere, if you look the cart table are very similar to order table.
Why you have to do this? Because you don't have an order before the confirmation by customer, so until the checkout is not complete that informations can't be saved in the order table.
So, first, create the field in database, in this case you have to add in ps_orders and in the ps_cart as well.
(In your case I suggest to use a DATETIME field)
Second, override the Order class:
class Order extends OrderCore
{
public function __construct($id = null, $id_lang = null)
{
self::$definition['fields']['deldate'] = array('type' => self::TYPE_DATE);
parent::__construct($id, $id_lang);
}
}
and the Cart class:
class Cart extends CartCore
{
public function __construct($id = null, $id_lang = null)
{
self::$definition['fields']['deldate'] = array('type' => self::TYPE_DATE);
parent::__construct($id, $id_lang);
}
}
Now we have to save the field during the checkout process, so we override the OrderController:
class OrderController extends OrderControllerCore
{
public function processAddress()
{
parent::processAddress();
// Here we begin our story
if(Tools::getIsset('deldate')) // Check if the field isn't empty
{
$deldate = Tools::getValue('deldate');
// Here you must parse and check data validity (I leave to you the code)
/* ... */
// Assign the data to context cart
$this->context->cart->deldate = $deldate;
// Save information
$this->context->cart->update();
}
}
}
Now you have to 'transport' this informations from the cart to the order, this will be done through the PaymentModule class, specifically with the validateOrder method.
So, another override:
class PaymentModule extends PaymentModuleCore
{
public function validateOrder($id_cart, $id_order_state, $amount_paid, $payment_method = 'Unknown', $message = null, $extra_vars = array(), $currency_special = null, $dont_touch_amount = false, $secure_key = false, Shop $shop = null)
{
$result = parent::validateOrder($id_cart, $id_order_state, $amount_paid, $payment_method, $message, $extra_vars, $currency_special, $dont_touch_amount, $secure_key, $shop);
if($result)
{
$oldcart = new Cart($id_cart);
$neworder = new Order($this->currentOrder);
$neworder->deldate = $oldcart->deldate;
$neworder->update();
return true; // important
}
else
{
return $result;
}
}
}
After all of this you have the deldate field saved. However, I absolutely don't suggest this method, it's more safe and simple with a module and hooks... But this is another story :)
This will works only with the five steps checkout.
For next code lines, God save me...
If you want to works with OPC you have to dirty your hands with JS and override the OrderOpcController.
Start with the JS, edit the order-opc.js in js folder of enabled theme, find bindInputs function and append this lines of code:
function bindInputs()
{
/* ... */
$('#deldate').on('change', function(e){
updateDelDateInput(); // custom function to update deldate
});
}
then append to the file your custom function:
function updateDelDateInput()
{
$.ajax({
type: 'POST',
headers: { "cache-control": "no-cache" },
url: orderOpcUrl + '?rand=' + new Date().getTime(),
async: false,
cache: false,
dataType : "json",
data: 'ajax=true&method=updateDelDate&deldate=' + encodeURIComponent($('#deldate').val()) + '&token=' + static_token ,
success: function(jsonData)
{
if (jsonData.hasError)
{
var errors = '';
for(var error in jsonData.errors)
//IE6 bug fix
if(error !== 'indexOf')
errors += $('<div />').html(jsonData.errors[error]).text() + "\n";
alert(errors);
}
// Here you can add code to display the correct updating of field
},
error: function(XMLHttpRequest, textStatus, errorThrown) {
if (textStatus !== 'abort')
alert("TECHNICAL ERROR: unable to save message \n\nDetails:\nError thrown: " + XMLHttpRequest + "\n" + 'Text status: ' + textStatus);
}
});
}
Then override the OrderOpcController, copy all the init method and change the line of code as below:
class OrderOpcController extends OrderOpcControllerCore
{
public function init()
{
// parent::init(); // comment or delete this line
FrontController::init(); // Very important!
// Then in this switch `switch (Tools::getValue('method'))` add your case
/* ... */
case 'updateDelDate':
if(Tools::isSubmit('deldate'))
{
$deldate = urldecode(Tools::getValue('deldate'));
// Here you must parse and check data validity (I leave to you the code)
/* ... */
// Assign the data to context cart
$this->context->cart->deldate = $deldate;
// Save information
$this->context->cart->update();
$this->ajaxDie(true);
}
break;
/* ... */
}
}
Obviously, is necessary the override of Order, Cart and PaymentModule as well.
PS: I hope that I didn't forget anything.
You can try also with this module
https://www.prestashop.com/forums/topic/589259-m%C3%B3dulo-selector-fecha-en-pedido/?p=2489523
Try this in the override of the class Order
class Order extends OrderCore
{
public function __construct($id = null, $id_lang = null)
{
parent::__construct($id, $id_lang);
self::$definition['fields']['deldate'] = array('type' => self::TYPE_STRING);
Cache::clean('objectmodel_def_Order');
}
}
The Cache::clean is need because getDefinition tries to retrieve from cache, and cache is set without the override on parent::__construct
I then tried to create a new empty Order and get the definition fields and it showed there, so it should save to mysql
$order = new Order();
var_dump(ObjectModel::getDefinition($order));exit;