Problem description

When we choose a product, we usually need to choose the corresponding product specifications to calculate the price. The price and inventory quantity selected by different specifications are different. For example, clothes have color, size and other attributes

The skU concept is referenced below

A Stock Keeping Unit (SKU) is an accounting term defined as the smallest available Unit in inventory management, for example, a SKU in textiles is usually a specification, color, style, and an item is sometimes referred to as an SKU in a chain retail store. Minimum inventory management unit can distinguish the smallest unit of different goods sales, is the scientific management of goods procurement, sales, logistics and financial management as well as POS and MIS system data statistics needs, usually corresponding to a management information system code. – the form wikipedia

So how should we add, edit and delete the specifications of goods in the background management system? Then we need to design a SKU specification generation component to manage our product specification Settings

The target

When we design a component, we need to know what the final result will be. Will the requirements meet us

As shown in the example in the figure, we need to design an infinite level to add specifications and specifications value, and then set the product price, cost price inventory and other information in the table, and finally meet our requirements

Analysis of the

Say from big field, specifications and forms list must be placed on different components inside, because of different processing logic and then each specification can only choose the specifications of the below value, and has been chosen specifications can no longer be choice, value is allowed to be deleted, specifications and specification of each specification bowdlerize will affect the contents of the table, but the specification is not affected by form, At the same time, the specifications can be added to…. indefinitely

Consider as many aspects of the component design as possible to be reasonable and possible

Then we also need to know what data types and methods the back end needs us to pass to the front end (which is very important). Suppose the back end needs to add specification data

{
    spec_format: Array<{
        spec_name: string;
        spec_id: number;
        value: Array<{
            spec_value_name: string,
            spec_value_id: number
        }>
    }> 
    
}Copy the code

Then set the price and stock data for each specification to

{ skuArray: Array<{ attr_value_items: Array<{ spec_id: number, spec_value_id: number }>; price: number; renew_price: number; cost_renew_price? : number; cost_price? : number; stock: number; } >}Copy the code

Here I divide the directory into the figure below

G-specification component that is used to manage specifications and tables. It is intended to be compared to their parent spec-price, which is a table component that sets prices, inventory, etc. Spec-item is a specification and spec-value is a selection component that lists the specification values for a specification

Specifications and components

Is my own personal habits like to display and view the relevant data such as components or not, including ngIf judgment fields and so on in a ViewModel data model alone, that’s good and apart from the rest of the interface to submit data, late and also facilitate the maintenance of others, I won’t detail here view of logical interaction

Start by creating a SpecModel

class SpecModel { 'spec_name': string = '' 'spec_id': number = 0 'value': Constructor () {} public setData(data: any): public setData(data: any): public setData(data: any): void { this['spec_name'] = data['spec_name']! =undefined? data['spec_name']:this['spec_name'] this['spec_name'] = data['spec_id']! =undefined? Data ['spec_id']:this['spec_id']} /* Public setValue(data: any[]): void { this['value'] = Array.isArray( data ) == true ? [...data] : [] } }Copy the code

Here I define a data model that is the same as the array subset in the spec_format field required by the back end. Each specification component is created with a new object like this. It is convenient to obtain specModels from multiple specification components in the G-specification component and assemble them into a spec_format array

Specification price and inventory setting components

The design of specification components varies from person to person. Common data is passed in and out. Data interaction between components may be Input or Output, or an EventEmitter can be created through services. Assume that at this point we have processed the specification component and the specification value list component and transferred the data through the g-specification.service file

In this component, I created a SpecDataModel model, which is used to unify the data source, and can handle data types and fields in the spec-Price component without missing or redundant etc

export class SpecDataModel {

    'spec': any = {}
    'specValue': any[] = []

    constructor( data: any = {} ){

        this['spec'] = data['spec'] ? data['spec']: this['spec']
        this['specValue'] = data['specValue'] ? data['specValue'] : this['specValue']

        this['specValue'].map(_e=>_e['spec']=this['spec'])

    }

}Copy the code

In this service, an EventEmitter is created to transfer data across components. The main data type is SpecDataModel

@Injectable()
export class GSpecificationService {

    public launchSpecData: EventEmitter<SpecDataModel> = new EventEmitter<SpecDataModel>()

    constructor() { }

}Copy the code

Each addition or deletion in the spec component is next to the spec data, the legend illustrates the removal of the spec, and each next data is received in the spec-price component

Public closeSpecValue(data: any, index: number): /* Public closeSpecValue(data: any, index: number): void { this.viewModel['_selectSpecValueList'].splice( index,1 ) this.gSpecificationService.launchSpecData.next( LaunchSpecDataModel (this.viewModel['_selectSpecValueList']))} public launchSpecDataModel(this.viewModel['_selectSpecValueList'])) specValue: any[], spec: SpecModel = this.specModel ): SpecDataModel { return new SpecDataModel( {'spec':spec,'specValue':[...specValue] } ) }Copy the code

The spec-Price component can then accept SpecDataModel data passed in from elsewhere

    this.launchSpecRX$ = this.gSpecificationService.launchSpecData.subscribe(res=>{
        // res === SpecDataModel
    })Copy the code

The data processing

Now that the spec-Price component has access to the data passed in by the spec component in real time, including the selected specifications and specification values, how do you process the data to fit the pattern of the combined table in the diagram and bind the price, cost, and inventory data to all specifications? Processing each specification operation results in the latest SpecDataModel. It is obvious that these SpecDataModel need to be consolidated into an array to store all the selected specifications

Obviously you still need to build a data model inside the component to handle the incoming SpecDataModel, so assume there is an _specAllData array to hold all the specs

We also observed that the table in the figure involves merging cells, which requires the ROWSPAN attribute of the TR tag (remember?).

Then it is analyzed again and found that the result of different number of specifications and specification values is a full permutation combination

Ex. :

Version: V1, V2, V3 Capacity: 10 people, 20 people

The result is 3 X 2 = 6, so the result presented in the table is 6, and if you add a size value, then the result is 3 X 3 = 9, so the table presentation involves the full permutation algorithm and rowSPAN calculation

Let’s create a SpecPriceModel data model

Class SpecPriceModel {'_title': string[] = [' new purchase price (yuan) ',' cost price (yuan) ',' Renewal price (yuan) ',' inventory '] Any [] = [] private 'constTitle': string[] = [...this._title]Copy the code

Since the last five columns of the table are fixed header, and each specification addition adds a header, you need to store the header in a variable. Although _specAllData can receive all specifications, it is also possible to duplicate data, and of course after all specifications have been removed, _specAllData should also have an empty array, so in SpecPriceModel you need to unduplicate _specAllData

public setAllSpecDataList( data: SpecDataModel ): void { if( data['specValue'].length > 0 ) { let _length = this._specAllData.length let bools: boolean = true for( let i: number = 0; i<_length; i++ ) { if( this._specAllData[i]['spec']['id'] == data['spec']['id'] ) { this._specAllData[i]['specValue'] = [...data['specValue']] bools = false break } } if( bools == true ) { this._specAllData.push( data ) } }else { this._specAllData = this._specAllData.filter( _e=>_e['spec']['name'] ! = data['spec']['name'] ) } this.setTitle() }Copy the code

Suppose the _specAllData we get at this time is

[{spec:{name: 'Version, ID: 1}, specValue:[{spec_value_id: 11, spec_value_name: 'v1.0'}, {spec_value_id: 111, spec_value_name: 'v2.0'}, {spec_value_id: 1111, spec_value_name: 'v3.0'}]}, {spec:{name: ' 2}, specValue:[{spec_value_id: 22, spec_value_name: '10 people '}, {spec_value_id: 222, spec_value_name: '20 people '}]}]Copy the code

So we just have to merge the cells and deal with all the permutations and combinations, and there’s actually a technical term for this algorithm called cartesian product

So I’m recursively dealing with all the possible permutations and combinations that exist in order

// let _recursion_spec_obj = (data: any)=>{let len: number = data.length if(len>=2){ let len1 = data[0].length let len2 = data[1].length let newlen = len1 * len2 let temp =  new Array( newlen ) let index = 0 for(let i = 0; i<len1; i++){ for(let j=0; j<len2; j++){ if( Array.isArray( data[0][i] ) ) { temp[index]=[...data[0][i],data[1][j]] }else { temp[index]=[data[0][i],data[1][j]] } index++ } } let newArray = new Array( len-1 ) for(let i=2; i<len; i++){ newArray[i-1]= data[i] } newArray[0]=temp return _recursion_spec_obj(newArray) } else{ return data[0] } }Copy the code

You get all the permutations and combinations that occur, in a two-dimensional array, called _mergeRowspan for now

[[{spec: {name: 'version', id: 1}, spec_value_id: 11, spec_value_name: 'v1.0}, {spec: {name:' capacity, id: 1}, spec_value_id: 22, spec_value_name: '10'}] / /...Copy the code

There are 3 X 2 = 6 possible outcomes

The ROWSPAN attribute of the TR tag specifies the number of rows that a cell can span.

As legend

V1.0 has 2 rows across, so its rowspan is 2. 10 and 20 people are the smallest cells, so rowspan is naturally 1



This time, V1.0 has a rowSpan of 4

Rowspan is 2 for 10 and 20 people

.

We can conclude that we simply calculate the rowSPAN value for each of the perpositions in the _mergeRowspan array and bind it bidirectively to the ROWSPAN of the TR tag when rendering the table

Calculate rowpsan

Take the figure above as an example, there are 12 cases where 3 X 2 X 2 = 12, where each value of the first specification occupies 4 rows, each value of the second specification occupies 2 rows, and each value of the last specification occupies one row

ForEach ((_e,_index)=>{this._tr_length *= _e['specValue'].length}) // Let _rowSPAN_divide = 1 for(let I: number = 0; i<this._specAllData.length; i++ ) { _rowspan_divide *= this._specAllData[i]['specValue'].length for( let e: number = 0; e<this._specAllData[i]['specValue'].length; e++ ) { this._specAllData[i]['specValue'][e]['rowspan'] = (this._tr_length)/_rowspan_divide } }Copy the code

The resulting data is shown in the figure below

Here, each piece of data knows its own rowspan value, so we can use *ngIf to determine what should and shouldn’t be displayed when rendering the table. Some people might say that this rowSpan concatenation is just fine with native DOM manipulation, but do you know how many rows it takes to manipulate these Rowspans?

Because rowspan 4 is 1/3 of the total 12, rowspan 2 is 1/6 of the total 12 in rows 1, 5, 5, 7, 9, and 11, so rowspan 1 is only in rows 1, 3, 5, 7, 9, and 11

So we have * ngIf judgment conditions for childen [‘ rowspan] = = 1 | | (I = = 0? true:i%childen[‘rowspan’]==0)

<tr *ngFor = "let list of tableModel['_mergeRowspan']; index as i"> <ng-container *ngFor = "let childen of list['items']; index as e"> <td class="customer-content" attr.rowspan="{{childen['rowspan']}}" *ngIf="childen['rowspan']==1||(i==0? true:i%childen['rowspan']==0)"> {{childen['spec_value_name']}} </td> </ng-container> </tr>Copy the code

Finally, a complete SpecPriceModel model is attached

Class TableModel {'_title': string[] = [' new purchase price ($) ',' cost price ($) ',' renew price ($) ',' inventory '] '_specAllData': Any [] = [] // all values passed by all specifications /* Merge all data and calculate the most existing TR tags. The rowPAN value is calculated as, previous specification = number of specification values multiplied by subsequent specification values */ '_mergeRowspan': any[] = [] '_tr_length': Number = 1 private 'constTitle': String [] = [...this._title] // Public setAllSpecDataList(data: SpecDataModel): void { if( data['specValue'].length > 0 ) { let _length = this._specAllData.length let bools: boolean = true for( let i: number = 0; i<_length; i++ ) { if( this._specAllData[i]['spec']['id'] == data['spec']['id'] ) { this._specAllData[i]['specValue'] = [...data['specValue']] bools = false break } } if( bools == true ) { this._specAllData.push( data ) } }else { this._specAllData = this._specAllData.filter( _e=>_e['spec']['name'] ! = data['spec']['name'])} this.settitle ()} /* Private setTitle(): void { let _title_arr = this._specAllData.map( _e=> _e['spec']['name'] ) this._title = [..._title_arr,... this.consttitle] this.handlemergerowSPAN ()} /**** Calculate specifications merge table unit *****/ private handleMergeRowspan():void ForEach ((_e,_index)=>{this._tr_length *= _e['specValue'].length}) Let _rowSPAN_divide = 1 for(let I: number = 0; i<this._specAllData.length; i++ ) { _rowspan_divide *= this._specAllData[i]['specValue'].length for( let e: number = 0; e<this._specAllData[i]['specValue'].length; E ++) {this._specallData [I]['specValue'][e][' rowSPAN '] = (this._tr_length)/ _rowSPAN_divide}} // Cartesian multiply let _recursion_spec_obj = ( data: any )=>{ let len: number = data.length if(len>=2){ let len1 = data[0].length let len2 = data[1].length let newlen = len1 * len2 let temp =  new Array( newlen ) let index = 0 for(let i = 0; i<len1; i++){ for(let j=0; j<len2; j++){ if( Array.isArray( data[0][i] ) ) { temp[index]=[...data[0][i],data[1][j]] }else { temp[index]=[data[0][i],data[1][j]] } index++ } } let newArray = new Array( len-1 ) for(let i=2; i<len; i++){ newArray[i-1]= data[i] } newArray[0]=temp return _recursion_spec_obj(newArray) } else{ return data[0] } } let _result_arr = this._specAllData.map( _e=>_e['specValue'] ) this._mergeRowspan = _result_arr.length == 1? (()=>{ let result: any[] = [] _result_arr[0].forEach(_e=>{ result.push([_e]) }) return result || [] })() : _recurSION_spec_obj (_result_arr) If (array.isarray (this._mergerowspan) == true) {this._mergerowspan = this._mergerowspan. Map (_e=>{return { Items: _e, costData: {price: 0.01, renew_price: 0.01, cost_renew_price: 0.01, cost_price: 0.01, stock: 1 } } }) }else{ this._mergeRowspan = [] } } }Copy the code

Compared to the traditional DOM operation roSPAN to dynamically merge tables, this method of computing rules and data binding is both shorter and easier to maintain

This article just abstractions the difficult parts of designing SKU components, and is of course just one approach that is easy to handle not only when adding specifications, but also when editing existing ones