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