⬆️ ⬇️

Visual editing of data on the page, using HTML as a data store

When we need to provide the user with the ability to graphically edit the content on a page, perhaps, most often we use JavaScript to store data and transfer it to the server, and all disputes are conducted around the display method, appearance of the editor. Our selection ranges from plain HTML (with or without canvas) to embedded SVG or the use of a Flash player.



It is not difficult to choose between these options: SVG is suitable for schemes or plans and other vector graphics, the canvas is more suitable for photos or other images. However, both of these elements require "separating" themselves from the page. By “branch,” I mean that any of these elements requires writing additional scripts to synchronize the view with the model.



For small objects whose structure is well described by a tree or list (for example, a shopping cart or a business process), using HTML elements to display and store data could simplify development and support.



')

All data is already on the page.





An HTML page is, first of all, a document, and not only the marking of graphic elements, it means that the page itself can contain all the data. If we consider the existence of 'data' attributes, then almost everything that can be represented by a string can be saved. At the same time, the server will be able to immediately give a fragment of the page for display and it can be inserted “as is” without additional processing. There are also advantages when sending data to the server: you can send it “as is” and pass through XSLT to get the XML document, send it as a “native” web form, or use JSON.



Imagine that we have a finished page; on it are presented a shelf with goods and a basket of a buyer realized by a simple list.



<ul id="store-shelf"> <li data-article="apple">Apple</li> <li data-article="milk">Milk</li> <li data-article="cacke">Cacke</li> <li data-topping="cherry" data-topping-article="cacke">Cherry Topping</li> <li data-topping="cream" data-topping-article="cacke">Cream Topping</li> </ul> <ul id="shop-cart"></ul> <input name="reset" type="button" value="Reset"/> <input name="submit" type="button" value="Submit"/> 




You may notice that some of the goods on the shelves are apples, milk and cakes, but also on the shelves are additional products (options) that can only be used in conjunction with other goods and not all of them (we have the opportunity to choose a cake with cream or cherry, or both). In this case, all the necessary information about the available products and their mutual connection is already on the page and it does not need to be stored separately in JavaScript.



Use script to manage items





Add a little jQuery UI and start dragging items from the shelf to the basket.



 $("#shop-cart").droppable({ accept: 'li[data-article]', activeClass: 'active', hoverClass: 'hover', drop: shopCartDropHandler }); $('#store-shelf li').draggable({ cursor: 'pointer', revert: true, stack: 'li' }); function shopCartDropHandler(event, ui) { var clone, counter, $this = $(this), article = ui.draggable.attr('data-article'), existingGoods = $this.children('[data-article="' + article + '"]:not(:has([data-topping]))'); if (existingGoods.length) { counter = existingGoods.find('input[name="quantity"]'); counter.val(parseInt(counter.val()) + 1); } else { counter = $('<input name="quantity" type="number" value="1" min="1"/>'); clone = ui.draggable.clone().css({left: 0, top: 0}).append(counter); clone.droppable({ accept: 'li[data-topping-article="' + article + '"]', activeClass: 'active', hoverClass: 'hover', drop: shopCartItemDropHandler }); $this.append(clone); } } function shopCartItemDropHandler(event, ui) { var span, $this = $(this), topping = ui.draggable.attr('data-topping'), toppingName = ui.draggable.text(), existingToppings = $this.find('span[data-topping="' + topping + '"]'); if (!existingToppings.length) { span = $('<span/>').attr('data-topping', topping).text(toppingName); $this.append(span); } } 




Here we added a kind of “grouping” (if the element has already been added, the counter simply increases) and introduced support for two types of elements: a product and an option. Now we collect a shopping cart. If you take the current fragment of the page with the basket, you can see that it fully describes its contents; All data is on the page without the need for additional storage on the client side (synchronization with the server is a different case and here we do not consider it). Below is an example of a finished basket (styles and attributes related to jQueryUI are omitted).



 <ul id="shop-cart" class="ui-droppable"> <li data-article="cacke">Cacke <input name="quantity" type="number" value="2" min="1"> <span data-topping="cherry">Cherry Topping</span> <span data-topping="cream">Cream Topping</span> </li> <li data-article="cacke">Cacke <input name="quantity" type="number" value="1" min="1"> <span data-topping="cherry">Cherry Topping</span> </li> <li data-article="milk">Milk <input name="quantity" type="number" value="2" min="1"> </li> <li data-article="apple">Apple <input name="quantity" type="number" value="1" min="1"> </li> </ul> 




It can be seen that we have an order for three cakes (two with cream and cherry and one with only cherry), two servings of milk and one apple.

The question remains: how to send this data to the server and how to display it back. To send, you can use the form (part of the 'data' attributes will need to be replaced with hidden input fields), or send an Ajax request with a JSON representation of the elements. The choice of one or another option depends on the complexity of the finished structure: a web form is not suitable for multilevel structures. Since we can have two levels in each position (main with a number and a set of options), we use JSON.



We will simply generate JSON, which reflects the existing DOM structure, extracting data from the attributes and properties of elements. At the same time, we ignore the “labels” and the names of products and options, since this information is not significant and can be easily and reliably restored on the server.



 $('input[name="submit"]').click(function () { var result = {items: []}; shopCart.find('li[data-article]').each(function () { var $this = $(this), toppings = [], article = { id: $this.attr('data-article') }, quantity = $this.find('input[name="quantity"]').val(); $this.find('span[data-topping]').each(function () { toppings.push({ id: $(this).attr('data-topping') }); }); result.items.push({ article: article, toppings: toppings, quantity: parseInt(quantity) }); }); $.ajax('/shopping-cart/webresources/cart', { cache: false, contentType: 'application/json', data: JSON.stringify(result), dataType: 'json', type: 'POST' }); }); 




Processing data on the server



Processing on the server is supposed to be quite simple: saving and issuing on request. To do this, we use the functionality provided by Glassfish and Jersey to create web services, add dependencies, and serialize JSON. We will create a REST service that will save the basket during a POST request and issue a saved shopping list during a GET request. Add one more object that will store the available products and options on the shelf and store the current basket. In a real application, these tasks will be undertaken by a database or other storage mechanism.



 @Path("cart") @Singleton @ApplicationScoped public class ShoppingResource { @Inject private Storage storage; @GET @Produces(MediaType.APPLICATION_JSON) public ShoppingCart get() { eturn storage.getShoppingCart(); } @POST @Consumes(MediaType.APPLICATION_JSON) public void post(ShoppingCart shoppingCart) { storage.setShoppingCart(shoppingCart);} } @Named @Singleton @ApplicationScoped public class Storage { private final Map<String, Article> articles = new HashMap<>(); private final Map<String, Topping> toppings = new HashMap<>(); private ShoppingCart shoppingCart; @PostConstruct public void init() { Article cacke = new Article("cacke", "Cacke"); putArticles(cacke, new Article("milk", "Milk"), new Article("apple", "Apple")); putToppings( new Topping("cream", "Cream Topping", cacke), new Topping("cherry", "Cherry Topping", cacke)); } private void putArticles(Article... articles) { for (Article article : articles) { his.articles.put(article.getId(), article); } } private void putToppings(Topping... toppings) { for (Topping topping : toppings) { this.toppings.put(topping.getId(), topping); } } @PreDestroy public void clear() { articles.clear(); toppings.clear(); } public Collection<Article> getArticles() { return articles.values(); } public Collection<Topping> getToppings() { return toppings.values(); } public ShoppingCart getShoppingCart() { return shoppingCart; } public void setShoppingCart(ShoppingCart shoppingCart) { ensureConsistence(shoppingCart); this.shoppingCart = shoppingCart; } private void ensureConsistence(ShoppingCart cart) { /*  “”     “”   */ } } 




We also need classes to read and write the serialized view of the basket. Below is a general abstract class that implements the basic functionality of reading / writing goods, options and basket items from JSON and back. For each particular type will use its own class based on this provider.



 @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) public abstract class JsonProvider<T> implements MessageBodyReader<T>, MessageBodyWriter<T> { private static final Logger LOGGER = Logger.getLogger(JsonProvider.class.getName()); private final ConcurrentMap<T, byte[]> serializedMap = new ConcurrentHashMap<>(); private final ObjectMapper mapper = new ObjectMapper(); protected final ObjectMapper getMapper() { return mapper; } protected abstract Class<T> getType(); // Reader Implementation @Override public final boolean isReadable(Class<?> type, Type type1, Annotation[] antns, MediaType mt) { return mt.equals(MediaType.APPLICATION_JSON_TYPE) && getMapper().canDeserialize(getJavaType()) && getType().isAssignableFrom(type); } private JavaType getJavaType() { return TypeFactory.fromClass(getType()); } @Override public T readFrom(Class<T> type, Type type1, Annotation[] antns, MediaType mt, MultivaluedMap<String, String> mm, InputStream in) throws IOException, WebApplicationException { return (T) getMapper().readValue(in, getClass()); } // Writer Implementation @Override public final boolean isWriteable(Class<?> type, Type type1, Annotation[] antns, MediaType mt) { return mt.equals(MediaType.APPLICATION_JSON_TYPE) && getMapper().canSerialize(getType()) && getType().isAssignableFrom(type); } @Override public final long getSize(T t, Class<?> type, Type type1, Annotation[] antns, MediaType mt) { byte[] result = new byte[0]; try { ByteArrayOutputStream stream = new ByteArrayOutputStream(); getMapper().writeValue(stream, t); result = stream.toByteArray(); serializedMap.put(t, result); } catch (IOException ex) { LOGGER.log(Level.SEVERE, null, ex); } return result.length; } @Override public final void writeTo(T t, Class<?> type, Type type1, Annotation[] antns, MediaType mt, MultivaluedMap<String, Object> mm, OutputStream out) throws IOException, WebApplicationException { if (serializedMap.containsKey(t)) { byte[] data = serializedMap.remove(t); out.write(data); } out.flush(); } } 




Now, to save the basket, we will use JSON, retrieving data directly from the DOM, and when the page loads, we will simply generate the necessary HTML fragments from the saved data on the server.



 <ul id="store-shelf"> <c:forEach var="article" items="#{storage.articles}"> <li data-article="${article.id}">${article.name}</li> </c:forEach> <c:forEach var="topping" items="#{storage.toppings}"> <li data-topping="${topping.id}" data-topping-article="${topping.article.id}">${topping.name}</li> </c:forEach> </ul> <ul id="shop-cart"> <c:forEach var="item" items="#{storage.shoppingCart.items}"> <li data-article="${item.article.id}">${item.article.name} <c:forEach var="topping" items="${item.toppings}"> <span data-topping="${topping.id}">${topping.name}</span> </c:forEach> <input name="quantity" type="number" value="${item.quantity}" min="1"/> </li> </c:forEach> </ul> 




As a result, we received a simple shopping basket, which to some extent implements the MVVM approach and does not require additional synchronization on the client side. In combination with a REST service that provides different formats depending on the client's request, we can use several ways to display data (for example, to give XML data to third-party services).



Of course, such a solution is suitable for simple cases where the model is well represented by a tree; naturally, for complex cases (with a large number of elements, or when the model does not fit into the HTML tree structure), other solutions will be required (for example, SVG or expanded canvas using third-party libraries) or something completely different than Flash or JavaFX.

Source: https://habr.com/ru/post/169577/



All Articles