DEV Community

Mark Davies
Mark Davies

Posted on • Edited on

Side project update #1

Starting my project

So I've had to do a lot of research to even get this far but man it has felt good! Let me take you through some of the decisions and code that I have completed up until now.

Feature set (right now)

  • Can make a table
  • Can make a paging control
  • Can replace data
  • Can pass in array of data

What's my focus for the next week?

  • Improving the paging control
    • Right now it will just print each number (if there was 100 pages it will render 100 buttons) what you want is the paging control that does 1, 2, 3 .... 100 and 1, 10, 11, 12... 100
  • Implementing a way to get remote information

Let's see some code :)

So first off I would like to talk through my options object, I know that sounds boring but let me explain:

export interface IDataGridOptions {
    dataSource: Array<object>;
    paging?: PagingOptions;
    columns? : Array<string>
}

export interface PagingOptions {
    pageSize: number;
}

Here you can see my set of options that you can pass into the data grid when you create it, the first issue with this is that you can have null objects which means that you will have to continually have to check null values. The second is that some of the options rely on other parts of the options. Let me explain my thought process behind this:

Null values

So the way I wanted to go about this was to have a class that has the same interface but the objects couldn't be null, I could use the interface to fill the object. This way I don't have to check for null all over the shop!

I don't know if you know but an interface does not have to have a declaration in typescript! You can create a dynamic object to represent the interface, time for an example I think:

Interface:

export interface IDataGridOptions {
    dataSource: Array<object>;
    paging?: PagingOptions;
    columns? : Array<string>
}

export interface PagingOptions {
    pageSize: number;
}

I can create a function that takes in that interface as a parameter:

export function myFunction(options : IDataGridOptions) : void {
}

Now if I was in a language like c# I would have to create a class that inherits from that interface to actually call that method, but in typescript you can create an object that represents the shape:

myFunction({
    dataSource : [],
    paging: {
    },
    columns : []
});

And this is perfectly valid code! So I don't have to deal with the null values in my code I thought it would be sensible to have a class that looks like this:


export class DataGridOptions {
    public dataSource : Array<object>;
    public columns : Array<string>;
    public paging : PagingOptions;

    constructor(){
        this.dataSource = new Array<object>();
        this.columns = new Array<string>();
        this.paging = {
            pageSize: 10
        }
    }

    public setColumns(columns : Array<string>) : DataGridOptions {
        this.columns = columns;
        return this;
    }

    public setPaging(paging:PagingOptions) : DataGridOptions {
        this.paging= paging;
        return this;
    }

    public setDataSource(dataSource: Array<object>) : DataGridOptions{
        this.dataSource = dataSource;
        return this;
    }
}

This is basically the same as the interface but it doesn't extend it, why is that? because the things that can be null in the interface can't be null here! Once I map the interface to the class I no longer have to worry about the null values, hooray!

So the question becomes how do we map one to the other?

Okay there are so many ways that we can do this, we can just get cracking with if statements in our code and that would be perfectly valid, I started out this way but it quickly became a maze of if statements and it became messy. So I turned to design patterns

Composite design pattern

I decided to use the composite design pattern, If you think of each object as a branch or a leaf it sort of fits, the kind of smell is that you have to continually pass around both the options that you are in the process of creating and the options that you are copying, but it does beat a massive block of if statements, time for and example again:

   abstract class Component {
        public add(component: Component): void {
        }

        abstract operation(options: IDataGridOptions, createdOptions: DataGridOptions): DataGridOptions;
    }

These are bad names I just haven't thought of anything better, maybe mapper and map? I'll come back to this though, stick with me because this is my base class!

class DefaultOptionsComponent extends Component {
        protected children: Component[] = [];

        public add(component: Component): DefaultOptionsComponent {
            this.children.push(component);
            return this;
        }

        public buildOptions(options: IDataGridOptions): DataGridOptions {
            let dataGridOptions = new DataGridOptions();
            return this.operation(options, dataGridOptions);
        }

        operation(options: IDataGridOptions, createdOptions: DataGridOptions): DataGridOptions {
            for (let i = 0; i < this.children.length; i++) {
                createdOptions = this.children[i].operation(options, createdOptions);
            }
            return createdOptions;
        }
    }

I kind of wanted to make operation protected so people couldn't accidently call it outside of our class but it meant that I couldn't call it from my component here, which was massively dissapointing. But anyway I want people to call buildOptions here, I create an empty DataGridOptions class and then start passing it around the classes children (which we will get to I promise).

class DefaultColumnsComponent extends Component {
        operation(options: IDataGridOptions, createdOptions: DataGridOptions): DataGridOptions {
            let columns = new Array<string>();
            if (options.columns == null || options.columns.length == 0) {
                columns = new Array<string>();
                for (const property in options.dataSource[0]) {
                    columns.push(property);
                }
            } else {
                columns = options.columns
            }
            return createdOptions.setColumns(columns);
        }
    }

A nice simple example of one of my composite classes, this is the columns builder - fi you haven't specified any of the columns that you want to render then I need to go get them from somewhere else, that somewhere else is obviously the data source, I take the keys from the properties push them into my array and then set them on the options that I am creating.

How does this hang together I hear you ask

export class DefaultOptionsDirector {
        public static build(options: IDataGridOptions): DataGridOptions {
            const tree = new DefaultOptionsComponent()
                .add(new DataSourceComponent())
                .add(new DefaultColumnsComponent())
                .add(new DefaultPagingComponent());
            return tree.buildOptions(options);
        }
    }

This is the nice bit, you have a director and you build a tree of components.

I don't know if I'll end up regretting this later but right now this seems like the cleanest way to do this, I might revisit this if it's causing issues

Dependency issues?

You can see here that the columns default options is dependent on the object that you pass in from the data options, which I'm thinking might cause me issues when I come to doing remote operations, I can't exactly grab them from an array when I implement the request/response side of things.

What I'm thinking right now is that the DataSourceComponent will take the data source from the interface and create a common object for me to use later on, if i's an array then I can simply do some in memory trickery, if it is a web call things might get a little funky especially with the paging being a later component, I may have to play with the ordering of the settings

another thing I might regret

What else have you done?

Oh man is this not enough!?
I think I want to talk about two more things, one minor mistake I made and one cool thing

Mistake you say?

Ah yes early on in the project I decided to take a dependency on Handlebars, great choice, I made everything apart of the initial template, the headers, the data, and the pager. Putting the pager as part of the template was my mistake, because I have to add on click actions to the pager buttons - what this means is that I have to create the html and then go and find specific parts of it (the buttons) and then add click buttons, this felt so wrong so I decided early on to cut this at the source and make it thought the pager was not apart of the template and simply add nodes onto the element once the initial data grid has been put into the html, this seemed to work pretty nicely!

What about the cool thing?

Ah yes one of my main things for this project is that I didn't want people to be dependent on multiple js files or anything, so building and minimizing it into one file was always going to be a goal for this project, I just didn't know when it was going to be a thing because I had a quick look around and it seemed that you could potentially do it via typescript witht some settings in the .tsconfig. Sadly adding the outFile setting to the tsconfig means that you have to then edit the module settings, either setting it to system or another value. But both of these settings immediately cause a bunch of issues for me. I decided to leave that as an "implementation detail" to be figured out in due course (I used glo boards to track things like this so I know I wouldn't forget).

But then I remembered someone talking to me about webpack a long time ago, so I decided to check it out, I managed to create a very minimalist config file:

const path = require('path');

module.exports = {
    entry: './src/BearBones.ts',
    module: {
        rules: [
            {
                test: /\.tsx?$/,
                use: 'ts-loader',
                exclude: /node_modules/
            }
        ]
    },
    resolve: {
        extensions: [ '.tsx', '.ts', '.js' ],
        alias: {
            'ejs': 'ejs.min.js'
        }
    },
    output: {
        filename: 'bear-bones.js',
        path: path.resolve(__dirname, 'dist')
    },
    node: {
        fs: "empty"
    }
};

Changed my package.json so that it now builds via webpack:

{
  "name": "js",
  "version": "1.0.0",
  "description": "",
  "main": "/src/BareBones.ts",
  "scripts": {
    "build": "webpack",
...
  },
  "author": "Mark Davies",
  "license": "MIT",
  "dependencies": {
    "handlebars": "^4.1.2"
  },
  "devDependencies": {
...
  }
}

And now it minimises itself into one file in my ./dist folder :D - very happy about this one!

Any issues you need to iron out?

Plenty! I think the biggest one right now is the fact that when I run npm test it fails, good news is that web storm allows me to run tests individually or as a suite so I've been living off of that but I think if I want to get people to work on the project I need to get that working!

Until next time

Hopefully I will keep working on this and keep people updated on the project through these posts, if you have any suggestions on how to make these better please leave a comment and I'll try and incorporate it into the next set of blog posts.

Check out the project here: https://github.com/joro550/bear-bones/tree/dev

Top comments (0)