Back to the beginning, I saw a piece of H5 compatible processing code embedded in a mobile terminal App many years ago, which is specifically compatible with the type of processing input box:

  • forAndroid 5.0.1, 5.0.2 timeType input fields are changed totextType (I still remember: there were only reset and cancel buttons on some mobile phones of these two versions, which was taken by customers);
  • Whether it’sIOSorAndroid.datetime,datetime-localAll usetextType, hand type is much more efficient than the default popbox selector.

Then a few days ago in Android 9.0 claw machine to try the default webView date and time selection, or the year of the stupid thick has not changed, the story began……

[TOC]

I embedded webView in Android App to do something indescribable, because the default native time selector in the webpage is too long and ugly, and there are too many uncertainties of compatibility, and the pop-up component of classification nature (commodity classification, province, city, such as the existence of superior and subordinate relationship) is too ugly, and I want to upgrade it. So I started designing and writing a generic selector.

Function:

It took more than 3 days to complete the appearance, which has a cameo appearance with ٩(❛ᴗ❛ danjun) danjun: P

Final effect:

This paper mainly plays a record and reference role, whether it is used in H5 mobile terminal design, Android, IOS interface design, including the interaction design, are meaningful; There are no plans to open source the library (it is too tightly integrated with my own library and relies on Swiper4).

First, function planning

I did not draw a picture beforehand, but I went over it in my head, and the functional picture above was drawn afterwards.

  1. Asynchrony must be supported because asynchrony can be used as synchronization, and synchronization can only be synchronized to death; The effect of asynchronous loading is better for large classified data. Time and date are asynchronous calls, but the data is returned immediately, so it is essentially synchronous.
  2. Support asynchronous initialization of default values, given a default value, asynchronous regardless of the classification of the default value can be selected.
  3. The popbox interface needs to have an empty button, which is equivalent to deleting the content of the input box, but when the user selects the action, this button is changed to return (watching many Android and H5 pickers, without exception, there is no such function, only return, but I think it is very important);
  4. Click blank area to return;
  5. Each column on the pop-up screen must support a header (header?). , each column can define its own name or easily identifiable logo;
  6. Beautiful, user operation in line with the mainstream mode of operation.

Second, the lowest base implementation

(1) Picker interface and function implementation

This is the lowest level, defining only the data format and presentation style, which is provided by the consumer through asynchronous callbacks.

Only responsible for:

  • √ Define the format of the received data and the received data;
  • √ Pop-up interface and update interface;
  • √ User interaction;

No matter:

  • What is the data?
  • × How much data there is;
  • × How many levels of data are there?1 - upEverything is ok, as long as the display is down;
  • × Data validity (including whether sub-level deletion is allowed);
  • X finer display appearance and control.

Therefore, it can be defined as (some of the excerpted comments and pseudocode are for reference only, the same below) :

Picker=func(set,onChange,onCancel){... } Set ={value:any // Initial value. Note: NULL is a special value that indicates that there is no choice. Other null values should be converted to this standard null value. For example, 0 needs to be converted to NULL
    ,title:"Title" // It can be hand-written HTML with custom styles
    
    ,columns: [// Define a selection column that limits the number of selection levels
        {
            name:"Column name"
            ,weight:1 // Column width weights.// More column style configurations},... ] .load:func // Loads the child list data of the specified option
        /* Load(onLoad,onError) vals=[level0,level1,....levelx] User calls onLoad(childs) when data is successfully loaded. Error callback onError(MSG), including */ if data is invalid
    ,resolve:func // The value of the initial value is reversely resolved to all levels. If the value of the initial value is null, no resolution call is made
        /* Resolve (value,onLoad,onError) onLoad(vals) [level0,level1... value] onError(MSG) */
}
Copy the code

Interface implementation reference most open source selector style, pick a beautiful picture and color matching ok; Finally, a relatively easy to use interface is summarized: the selector shows 7 lines of candidate items, each line 45px, in view and operation are relatively good; The GIF above is set to 5 lines for screenshot purposes; Also note that if the scroll component does not have a callback, we can monitor the selected position to force a refresh of the screen. If swiper moves too fast occasionally, the callback will be lost.

The biggest challenge was dealing with the complexity of configuration items and composition logic in the interface, but I have less than 100 lines of outdated HTML template parsing engine, so I can handle the interface well. After writing the selector, I updated the document and went to GitHub BuildHTML to see how it works.

(2) Basic implementation of different types of selectors

Picker is done, but it would be too complicated for each type of selector to call Picker directly for different data sources. For example, time and date operations can share much of the same code, and the load and resolve data request sections of the asynchronous type can be wrapped once.

So it’s divided into three parts:

  1. Time date class, this part is divided intoTime,Date,DateTime, they have some logic to share, such as time calculation, but the interface is different, so it is not decomposed here, and will be decomposed in the later data source.
  2. Synchronization class Type SyncThis type of data is all prepared in advance and does not existload,resolveComplex asynchronous operations; Although the same specific type of interface is exactly the same as the asynchronous one, it is still a separate category.
  3. Asynchronous class Type AsyncGood packaging,load,resolveThese low-level onerous operations are used by upper-layer concrete types.

Synchronization class,Asynchronous classThe two methods are defined as:

PickerType=func(set,onChange,onCancel)value:123 / / the default value
    ,title:"Please select"
    ,data: {}// Mandatory, complete type data. The specific data format is uniformly defined here, and is automatically converted to the format required by the Picker
    
    ,allowLose:false // If the option does not have a lower level, it is not allowed. If the lower level is missing, the 'load' will go directly to the error callback
    
    ,columns: []// Mandatory, for the picker.columns option
    
    ,picker: {}Columns and title are not used here
    
    ,itemFormat:func // Format options, such as option names
    ,itemsSort:func // Sort the list of options
}
Copy the code
PickerTypeAsync=func(set,onChange,onCancel) the most core setting reference:set={extend pickertype. set +* -data // Same as PickerType, but without datatype:Load Data type to load// Load, resolve should call an interface of the backend unified, throughtypeHotData :[] hotData:[] hotData:[] hotData:[] hotData:[] hotData:[] hotData:[] hotData:[Copy the code

Data source layer

(1) Time and date

Time, Date, DateTime selectors have similar data except for different interfaces:

  1. I can limit the size interval;
  2. DateTimeThat includes the calculation ofTime,DateRealization of two;

(1) Generate table data at year, month, and day levels by providing 0 to 2 levels:

[] Generated year list VALS =[2010] Generated year list vals=[2010] Generated month list vals=[2010 2] Generated February 2010 list GenDate ({min:new Date("2012-01-01"), Max :new Date("2012-02-06")},[2012,2]
function genDate(set,vals){
    var min=set.min;
    var max=set.max;
    
    var a,b;
    var minY=min.getFullYear(),maxY=max.getFullYear();
    var minM=min.getMonth()+1,maxM=max.getMonth()+1;
    var y=vals[0],m=vals[1];
    var fixed=2 -;
    if(vals.length==0){
        a=minY;
        b=maxY;
        fixed=4 -;
    }else if(vals.length==1){ a=y==minY? minM:1; b=y==maxY? maxM:12;
    }else{ a=y==minY&&m==minM? min.getDate():1;
        if(y==maxY&&m==maxM){
            b=max.getDate();
        }else{
            if("| 1 | 3 | 5 7 8 10 | | | | | 12".indexOf("|"+m+"|") +1){
                b=31;
            }else if(m==2) {if(y % 4= =0 && y % 100! =0 || y % 400= = =0){
                    b=29;
                }else{
                    b=28;
                };
            }else{
                b=30;
            };
        };
    };
    
    var rtv=[];
    for(vari=a; i<=b; i++){ rtv.push({text: ("0"+i).substr(fixed)
            ,value:i
        });
    };
    return rtv;
};
Copy the code

(2) By providing 0-1 (Time) or 3-4 (DateTime) levels through [hour] or [year, month, day, hour], the table data can be generated in two levels of Time and Time:

Set (year, month, day, and hour values) (the first three values are DateTime (if not DateTime), and (if not DateTime) (if not DateTime) (if not DateTime) (if not DateTime) (if not DateTime) GenTime ({min:10*60+56, Max :21*60+3},[21]) GenTime ({min: new Date (" the 2012-01-01 10:56 "), Max: new Date (" 2012-02-06 21:03 ")},,2,6,21 [2012]) * /
function genTime(set,vals){
    var min=set.min;
    var max=set.max;
    var h=vals[0];
    if(vals.length>2) {//DateTime
        var y=vals[0],m=vals[1],d=vals[2];
        if(y==min.getFullYear()&&m==min.getMonth()+1&&d==min.getDate()){
            min=min.getHours()*60+min.getMinutes();
        }else{
            min=0;
        };
        if(y==max.getFullYear()&&m==max.getMonth()+1&&d==max.getDate()){
            max=max.getHours()*60+max.getMinutes();
        }else{
            max=23*60+59;
        };
        h=vals[3];
    };
    
    var a,b;
    var minH=Math.floor(min/60),maxH=Math.floor(max/60);
    if(h==null){
        a=minH;
        b=maxH;
    }else{ a=h==minH? min%60:0; b=h==maxH? max%60:59;
    };
    
    var rtv=[];
    for(vari=a; i<=b; i++){ rtv.push({text: ("0"+i).substr(2 -),value:i
        });
    };
    return rtv;
};
Copy the code

With these two methods, we can write concrete implementations of three types:

PickerTime=func(set,onChange,onCancel)
PickerDate=func(set,onChange,onCancel)
PickerDateTime=func(set,onChange,onCancel)

3All the core Settings are similar: set={min:123 ||"Prefer" // Minimum time
    max:123 ||"23:59." // Maximum time
    value:123 ||"10:01" // Set the time, if null is the current time part
    
    title:"Choose the time"
    picker:{} //Picker more configuration items} PickerTime load:function(vals,onLoad,onError){
    onLoad(genTime(set,vals));
}

PickerDate
    onLoad(genDate(set,vals));
    
PickerDateTime
    onLoad(vals.length>2? genTime(set,vals):genDate(set,vals));Copy the code

The Picker methods that these three types call directly generate columns, load, and reverse configuration items internally without requiring the user to deal with the most complex configuration at the bottom.

(2) Multi-level synchronous classification, such as city

Because the Type Sync selector for the synchronization class is already implemented on top of the Picker, we simply call the PickerType synchronization method directly, passing in the classification data.

For example, for provincial level 3 selection, we can load the city, provincial level 3 data into the page.

(3) Multi-level asynchronous classification, such as city

Since TypeAsync is implemented for asynchronous classes on top of the Picker, we simply call the PickerTypeAsync synchronous method directly and pass in the Type to be loaded asynchronously. The Type can be cities like provinces, cities, commodity categories, or even the odd category.

For example, to select level 4 of province, city and town, we just need to set type=”city” and so on. In order to improve the response speed, provincial and urban level 3 can be loaded as thermal data in advance.

GitHub AreaCity-JsSpider-StatsGov urban town data, which has been updated very quickly in the past year, I am using it myself, and there are nearly 1000 star.

Fourth, the final call layer

If you use the Picker directly, it’s excruciating because you have to write complex data loading and parsing functions.

Hence the next level of encapsulation: PickerTime, PickerDate, PickerDateTime, PickerType, PickerTypeAsync.

However, these functions still need to be called manually, which is not easy enough. I want to:

  1. Give aDom node(such as input box), assign a city ID, automatically converted into the province name display;
  2. Click on theDom node, automatically pop up the selection, automatically update the name display after selection;

So I further encapsulated the Picker* to get the top two layers, which are really used and rarely call Picker* that is too low-level.

These two layers were written using one of my favorite writing habits, and I’m not going to tell you anything about it, because I’m not going to tell you anything about it. The end result is the clickable, clickable Picker forms in the GIF at the beginning of this article. If you’re interested, take a look at the DOM nodes in the console to see how this works.


Finish > <