This article introduces the implementation idea of the common images on the web page to directly generate small images previews on the page after uploading them. Considering that this function has certain applicability, the relevant logic is encapsulated into an ImageUploadView component. The actual use effect can be used to view the git renderings in the next paragraph. In the process of implementing this component, we use the relevant content introduced in the previous blogs, such as inheritance library class.js, and the event management library eventBase.js of any component, which also includes some thoughts on the separation of responsibilities, separation of performance and behavior. Welcome to read and communicate.
Demonstration effect:
Note: Since the codes for the demonstration are all static, the component uploaded by the file is simulated by setTimeout, but its calling method is exactly the same as when I use the upload component in my actual work, so the code implementation of the demonstration effect is completely in line with the real functional requirements.
According to my previous blog ideas, let’s first introduce the requirements for this upload preview function.
1. Requirements Analysis
According to the previous demonstration renderings, the analysis requirements are as follows:
1) At the initial upload area, only one clickable upload button is displayed. When clicking this button, the uploaded image will be displayed in the subsequent preview area.
2) After the uploaded image is added to the preview area, you can remove it by deleting the button.
3) When the total number of uploaded images reaches a certain limit, for example, the upload limit in the demonstration is 4, remove the upload button;
4) When the total number of uploaded pictures reaches a certain limit, if a certain picture is removed through the deletion operation, the upload button must be displayed.
The above requirements are visible. Based on experience, the requirements that can be analyzed are as follows:
1) If the page is in editing state, that is, the state queryed from the database, as long as the image list is not empty, the image must be displayed at the beginning; and it must also control whether the upload button is displayed according to the length of the found image list and the upload limit;
2) If the current page is a state that can only be viewed but not changed, then the upload button and the delete button must be removed at the initial stage.
After the requirements analysis is completed, let me explain my implementation ideas.
2. Implementation ideas
Since this is a form page, if you want to submit it to the background after uploading the image, you must need a text field. So when I was doing a static page, I took this text field into consideration. After uploading a new image and deleting the image, I had to modify the value of this text field. The structure of this part when static pages was made is as follows:
<div><label>Electronic version of the legal person ID card</label><div><input id="legalPersonIDPic-input" name="legalPersonIDPic"type="hidden"><ul id="legalPersonIDPic-view"><li><a href="javascript:;">+</a></li></ul><p>Please make sure the pictures are clear and the text is identifiable<a href="#"><i></i> View example</a></p></div></div>
From this structure, we can also see that I put the entire upload area in one ul, and then use the first ul li as the upload button. In order to complete this function, our main tasks are: uploading and uploading callbacks, adding or deleting image previews, and managing text field values. From this point of view, combined with the idea of separation of responsibilities, this function requires at least three components, one responsible for file upload, one responsible for image preview management, and the other responsible for text domain values management. Never encapsulate these three functions in pairs or all together. In that case, the functional coupling is too strong and the components written are extensible and reusable. If interaction is required between these three components, we just need to define them to externally called interfaces with the help of callback functions or publish-subscribe mode.
However, the management of text domain values is very simple, and it doesn't matter whether it is written as a component or not, but at least function-level encapsulation is required. Although the file upload component is not the focus of this article, there are many ready-made open source plug-ins on the Internet, such as webuploader, which can be applied whether it is used directly or secondary encapsulation. The function of image preview is the core content of this article. The ImageUploadView component is the encapsulation of it. From the requirements point of view, there are only three semantic example methods for this component, namely render, append, and delItem. The render is used to display the initial preview list after initialization is completed, append is used to add a new image preview after uploading, and delItem is used to delete existing image previews. According to this basic idea, we only need to design options and events for it based on the requirements and experience in component development.
From the previous requirements, we found that the render of this ImageUploadView component will be affected by the page status. When the page is in view mode, this component cannot upload and delete, so you can consider adding a readonly option to it. At the same time, its upload and deletion operations will also affect the UI logic of the upload button, which is related to the upload limit. For flexibility, the upload limit must also be regarded as an option. From the three instance methods mentioned in the previous paragraph, according to your previous experience in defining events, an instance method generally defines a pair of events, just like the plug-in of bootstrap. For example, the render method can define a render.before. This event is triggered before the main logic of the render is executed. If the external listener calls the preventDefault() method of this event, then the main logic of the render will not be executed; there is also a render.after event, which is triggered after the main logic of the render is executed. The advantage of this pairwise definition event is that it not only provides external methods to extend component functionality, but also increases the management of component default behavior.
Finally, from my previous work experience, in addition to uploading pictures for previewing, I have also done uploading videos, uploading audio, uploading ordinary documents, etc., so when I encountered this function this time, I felt that I should extract similar things from these functions as a base class, image uploading, video uploading, etc., respectively inherit this base class to implement their respective logic. Another advantage of this base class is that it can completely separate the general logic from the HTML structure. In this base class, only common things are done, such as the definition of options and component behavior (render, append, delItem), as well as the listening and triggering of general events. It only needs to leave a fixed interface for the subclass to implement. In the subsequent implementation, I defined a FileUploadBaseView component to complete the functions of this base class. This base class does not contain any logic for html or css processing. It just abstracts the functions we want to complete and does not handle any business logic. Subclasses implemented according to business logic will be limited by the html structure, so the scope of application of subclasses is small; and because the base class is completely separated from the html structure, it has a larger scope of application.
3. Implementation details
From the implementation idea of Part 2, the classes to be implemented are: FileUploadBaseView and ImageUploadView, the former is the base class of the latter. At the same time, considering that the component needs to provide event management functions, we need to use eventBase.js in the previous blog, and FileUploadBaseView must inherit the EventBase component of the library; considering that there is a class definition and inheritance, we also need to use the inheritance library class.js written before to define the component and the inheritance relationship of the component. The inheritance relationship of related components is: ImageUploadView extend FileUploadBaseView extend EventBase.
(Note: The following related code uses seajs in modularity.)
What FileUploadBaseView does are:
1) Define common options and common event management
In the DEFAULTS configuration of this component, you can see the definitions of all common options and common events:
var DEFAULTS = {data: [], //The list of data to be displayed must be of object type, such as [{url: 'xxx.png'},{url: 'yyyy.png'}]sizeLimit: 0, //To limit the number of elements displayed in BaseView, to 0, it means that it does not limit readonly: false, //To control whether elements in BaseView allow addition and deletion onBeforeRender: $.noop, //ForeReport.before event, trigger onRender: $.noop, //ForeReport.after event before the render method is called, and onBeforeAppend: $.noop, //Complied with the append.before event, trigger onAppend: $.noop, //Complied with the append.after event, trigger onBeforeDelItem: $.noop, //Complied with the delItem.before event, trigger onDelItem: $.noop before delItem method is called //Complied with the delItem.after event, triggered };In the component's init method, you can see the initialization logic for general option and event management:
init: function (element, options) {//Call the init method of the parent class EventBase through this.base;//Instance attribute var opts = this.options = this.getOptions(options); this.data = resolveData(opts.data); delete opts.data; this.sizeLimit = opts.sizeLimit; this.readOnly = opts.readOnly;//Binding event this.on('render.before', $.proxy(opts.onBeforeRender, this)); this.on('render.after', $.proxy(opts.onRender, this)); this.on('append.before', $.proxy(opts.onBeforeAppend, this)); this.on('append.after', $.proxy(opts.onAppend, this)); this.on('delItem.before', $.proxy(opts.onBeforeDelItem, this)); this.on('delItem.after', $.proxy(opts.onDelItem, this));},2) Define the behavior of components and reserve interfaces that can be implemented by subclasses:
render: function () {/*** render is a template. The subclass does not need to override the render method, it only needs to override the _render method* When the subclass's render method is called, the render method of the parent class is called* But when executing the _render method, the _render method of the subclass is called* This will unify the triggering operations of before and after events*/var e; this.trigger(e = $.Event('render.before'));if (e.isDefaultPrevented()) return; this._render(); this.trigger($.Event('render.after'));},//Subclasses need to implement the _Render method_render: function () {},append: function (item) {var e;if (!item) return;item = resolveDataItem(item);this.trigger(e = $.Event('append.before'), item);if (e.isDefaultPrevented()) return;this.data.push(item);this._append(item);this.trigger($.Event('append.after'), item);},//Subclasses need to implement the _append method_append: function (data) {},delItem: function (uuid) {var e, item = this.getDataItem(uuid);if (!item) return;this.trigger(e = $.Event('delItem.before'), item);if (e.isDefaultPrevented()) return;this.data.splice(this.getDataItemIndex(uuid), 1);this._delItem(item);this.trigger($.Event('delItem.after'), item);},//Subclasses need to implement _delItem method_delItem: function (data) {}In order to uniformly handle the event distribution logic before and after the behavior, the main logic of render, append, delItem is extracted into methods _render, _append and _delItem that need to be implemented by subclasses. When the render method of a subclass is called, the method of the parent class is actually called, but when the parent class executes the _render method, the method of the subclass is executed, and the other two methods are also similar. It should be noted that subclasses cannot override the three methods render, append, and delItem, otherwise you have to handle the trigger logic of related events by yourself.
The overall implementation of FileUploadBaseView is as follows:
define(function (require, exports, module) {var $ = require('jquery');var Class = require('mod/class');var EventBase = require('mod/eventBase');var DEFAULTS = {data: [], //To display the list of data, the list elements must be of object type, such as [{url: 'xxx.png'},{url: 'yyyy.png'}]sizeLimit: 0, //To limit the number of elements displayed in BaseView, if 0 means no limit readonly: false, //To control whether elements in BaseView are allowed to be added and deleted onBeforeRender: $.noop, //corresponding render.before event, trigger onRender: $.noop, //corresponding render.after event, trigger onBeforeAppend: $.noop, //corresponding append.before event, trigger onAppend: $.noop, //corresponding append.after event, trigger onBeforeDelItem: $.noop, //corresponding append.after event, trigger onBeforeDelItem: $.noop, //corresponding append.after event, trigger onBeforeDelItem: $.noop, //corresponding delItem.before event, trigger onDelItem: $.noop //corresponding delItem.after event, trigger };/*** Data processing, add a _uuid attribute to each record of data to facilitate searching */function resolveData(data) {var time = new Date().getTime();return $.map(data, function (d) {return resolveDataItem(d, time);});}function resolveDataItem(data, time) {time = time || new Date().getTime();data._uuid = '_uuid' + time + Math.floor(Math.random() * 100000);return data;}var FileUploadBaseView = Class({instanceMembers: {init: function (element, options) {//Call the init method of the parent class EventBase through this.base;//Instance attribute var opts = this.options = this.getOptions(options); this.data = resolveData(opts.data); delete opts.data; this.sizeLimit = opts.sizeLimit; this.readOnly = opts.readOnly;//Binding event this.on('render.before', $.proxy(opts.onBeforeRender, this)); this.on('render.after', $.proxy(opts.onRender, this)); this.on('append.before', $.proxy(opts.onBeforeAppend, this)); this.on('append.after', $.proxy(opts.onAppend, this)); this.on('delItem.before', $.proxy(opts.onBeforeDelItem, this)); this.on('delItem.after', $.proxy(opts.onDelItem, this));},getOptions: function (options) {return $.extend({}, this.getDefaults(), options);},getDefaults: function () {return DEFAULTS;},getDataItem: function (uuid) {//Get dateItemreturn this.data.filter(function (item) {return item._uuid === uuid;})[0];},getDataItemIndex: function (uuid) {var ret;this.data.forEach(function (item, i) {item._uuid === uuid && (ret = i);});return ret;},render: function () {/*** render is a template. The subclass does not need to override the render method, it only needs to override the _render method* When the subclass render method is called, the render method of the parent class is called* But when the _render method is executed, the _render method of the subclass is called* This will unify the triggering operations of before and after events*/var e; this.trigger(e = $.Event('render.before'));if (e.isDefaultPrevented()) return; this._render(); this.trigger($.Event('render.after'));},//Subclasses need to implement the _Render method_render: function () {},append: function (item) {var e;if (!item) return;item = resolveDataItem(item);this.trigger(e = $.Event('append.before'), item);if (e.isDefaultPrevented()) return;this.data.push(item);this._append(item);this.trigger($.Event('append.after'), item);},//Subclasses need to implement the _append method_append: function (data) {},delItem: function (uuid) {var e, item = this.getDataItem(uuid);if (!item) return;this.trigger(e = $.Event('delItem.before'), item);if (e.isDefaultPrevented()) return;this.data.splice(this.getDataItemIndex(uuid), 1);this._delItem(item);this.trigger($.Event('delItem.after'), item);},//Subclasses need to implement the _delItem method_delItem: function (data) {}}, extend: EventBase,staticMembers: {DEFAULTS: DEFAULTS}});return FileUploadBaseView;});The implementation of ImageUploadView is relatively simple, similar to filling in the blanks. There are a few points to explain:
1) The DEFAULTS of this class needs to extend the DEFAULTS of the parent class to add the default options of this subclass, and also retain the definition of the default options of the parent class; according to the static page structure, an onAppendClick event has been added, and the relevant methods of file upload components can be called externally in this event:
//Inherit and extend the default DEFAULTSvar of the parent class DEFAULTS = $.extend({}, FileUploadBaseView.DEFAULTS, {onAppendClick: $.noop //Callback when clicking the upload button});2) In the init method, the init method of the parent class needs to be called to complete those general logical processing; at the end of init, you have to manually call the render method so that you can see the effect after the component is instantiated:
Other implementations are purely business logic implementations and are closely related to the requirements of Part 2.
The overall implementation of ImageUploadView is as follows:
define(function (require, exports, module) {var $ = require('jquery');var Class = require('mod/class');var FileUploadBaseView = require('mod/fileUploadBaseView');//Inherit and extend the default DEFAULTSvar DEFAULTS = $.extend({}, FileUploadBaseView.DEFAULTS, {onAppendClick: $.noop //Callback when clicking the upload button});var ImageUploadView = Class({instanceMembers: {init: function (element, options) {var $element = this.$element = $(element);var opts = this.getOptions(options);//Call the init method of the parent class to complete the options acquisition, data parsing, and listening processing of general events this.base(this.$element, options);//Add the upload and delete listeners and trigger processing if (!this.readOnly) {var that = this;that.on('appendClick', $.proxy(opts.onAppendClick, this));$element.on('click.append', '.view-act-add', function (e) {e.preventDefault(); that.trigger('appendClick');});$element.on('click.remove', '.view-act-del', function (e) {var $this = $(e.currentTarget); that.delItem($this.data('uuid'));e.preventDefault();});}this.render();},getDefaults: function () {return DEFAULTS;},_setItemAddHtml: function () {this.$element.prepend($('<li><a href="javascript:;">+</a></li>'));},_clearItemAddHtml: function ($itemAddLi) {$itemAddLi.remove();},_render: function () {var html = [], that = this;//If it is not a read-only state and has not reached the upload limit, add the upload button if (!(this.readOnly || (this.sizeLimit && this.sizeLimit <= this.data.length))) {this._setItemAddHtml();}this.data.forEach(function (item) {html.push(that._getItemRenderHtml(item))});this.$element.append($(html.join('')));},_getItemRenderHtml: function (item) {return ['<li id="',item._uuid,'"><a href="javascript:;"><img src="',item.url,'">',this.readOnly ? '' : '<span data-uuid="',item._uuid,'">Delete</span>','</a></li>'].join('');},_dealWithSizeLimit: function () {if (this.sizeLimit) {var $itemAddLi = this.$element.find('li.view-item-add');//If the upload limit has been reached, remove the upload button if (this.sizeLimit && this.sizeLimit <= this.data.length && $itemAddLi.length) {this._clearItemAddHtml($itemAddLi);} else if (!$itemAddLi.length) {this._setItemAddHtml();}}},_append: function (data) {this.$element.append($(this._getItemRenderHtml(data)));this._dealWithSizeLimit();},_delItem: function (data) {$('#' + data._uuid).remove();this._dealWithSizeLimit();}}, extend: FileUploadBaseView}); return ImageUploadView;});4. Demo Instructions
The demo project structure is:
What is framed is the core code of the demonstration. Among them, fileUploadBaserView.js and imageUploadView.js are the two core components implemented in the previous implementation. fileUploader.js is used to simulate uploading components. Its instance has an onSuccess callback, indicating that the upload is successful; there is also an openChooseFileWin used to simulate the process of opening the selection file window and uploading it:
define(function(require, exports, module) {return function() {var imgList = ['../img/1.jpg','../img/2.jpg','../img/3.jpg','../img/4.jpg'], i = 0;var that = this;that.onSuccess = function(uploadValue){}this.openChooseFileWin = function(){setTimeout(function(){that.onSuccess(imgList[i++]);if(i == imgList.length) {i = 0;}},1000);}}});app/regist.js is the logical code of the demonstration page, and the key parts have been explained with comments:
define(function (require, exports, module) {var $ = require('jquery');var ImageUploadView = require('mod/imageUploadView');var FileUploader = require('mod/fileUploader');//This is a file upload component simulated by asynchronous tasks//$legalPersonIDPic, which is used to store uploaded file information. After the upload component is successfully uploaded and after the ImageUploadView component deletes a certain item, it will affect the value of $legalPersonIDPic var $legalPersonIDPic = $('#legalPersonIDPic-input'),data = JSON.parse($legalPersonIDPic.val() || '[]');//data is the initial value. For example, the current page may be loaded from the database and needs to be presented with the ImageUploadView component//After the file upload is successfully uploaded, save the newly uploaded file to the value of $legalPersonIDPic//$legalPersonIDPic stores var appendImageInputValue = function ($input, item) {var value = JSON.parse($input.val() || '[]');value.push(item);$input.val(JSON.stringify(value));};//When the ImageUploadView component is called to delete an item, the stored information in $legalPersonIDPic should be cleared synchronously. var removeImageInputValue = function ($input, uuid) {var value = JSON.parse($input.val() || '[]'), index;value.forEach(function (item, i) {if (item._uuid === uuid) {index = i;}});value.splice(index, 1);$input.val(JSON.stringify(value));};var fileUploader = new FileUploader();fileUploader.onSuccess = function (uploadValue) {var item = {url: uploadValue};legalPersonIDPicView.append(item);appendImageInputValue($legalPersonIDPic, item);};var legalPersonIDPicView = new ImageUploadView('#legalPersonIDPic-view', {data: data,sizeLimit: 4,onAppendClick: function () {//Open the window to select the file fileUploader.openChooseFileWin();},onDelItem: function (data) {removeImageInputValue($legalPersonIDPic, data._uuid);}});});5. Summary of this article
The ImageUploadView component is not difficult to implement in the end, but I also spent a lot of time thinking about it and other parent classes to implement it, and most of the time was spent abstracting the separation of responsibilities and behavioral separation. The views on these two aspects of programming ideas expressed in this article are only my own personal experience. Because of the abstract level, everyone's thinking methods and the final understanding will not be the same. Therefore, I cannot directly say whether I am right or wrong. The purpose of writing is to share and communicate, and see if there are other experienced friends who are willing to tell you about their ideas in this regard. I believe that after everyone has read too much about other people's ideas, they will also help their own programming ideas training.