Creating Better PCF Component – Part 3

With all the things settled, now we can continue our journey to make our PCF-Component better. The last commit that we submitted, there are few things that we can enhance:

  1. When you choose an image file, then click the Submit button. We can reset the input file.
  2. Hide the image-element if no image has been upload.
  3. Add image-resizer to make our image smaller in our database.

Reset Input File

After successfully convert, the file input not reset

After successfully convert the image file to Base64String, we can reset our input file to notify the user if the operation success/failed. To make this happens, sure we will add the test first.

file-upload.spec.test:

it('can set input-file to empty', async () => {
    const file = new fileUpload(context, htmlDivElement, () => { });
    file.generate();
    const input = document.getElementById(controls.file) as HTMLInputElement;
    input.setAttribute('value', __dirname + 'img/sample.jpg');
    file.clearFile();
    expect(input.getAttribute('value')).to.equal('');
});

file-upload.test:

clearFile() {
    const imageFile = document.getElementById(
        controls.file
    ) as HTMLInputElement;

    imageFile.setAttribute('value', '');

    if(imageFile.value) {
        imageFile.value = '';
    }
}

If you see clearly on method clearFile, sure you will see there is a weird code imageFile.setAttribute(‘value’, ”) follow-up with imageFile.value = ”. The setAttribute is needed by the test, while the second code is for the real implementation. Sometimes as a TDD Developer, we will find that we need to take some alternative ways to make our code + tests work (if we just put imageFile.value = ”, our test will throw an error InvalidState because the input file only accepts valid file path). I need to stress this point because even though it’s not an ideal solution, but for current is adequate.

To implement clearFile method, we can put this method after we successfully convert our image to Base64String using Promise nature (then only will be called if the operation succeed):

submit(): void {
    const imageFile = document.getElementById(
        controls.file
    ) as HTMLInputElement;
    const information = document.getElementById(controls.information);
    if (!information) return;

    const filePath = imageFile.value;
    information.style.display = !filePath ? 'block' : 'none';

    if (!filePath) {
        return;
    }

    const file = imageFile.files![0];
    this.convertToBase64(file as Blob).then(base64String => {
        this.clearFile();
        return this.resize(base64String);
    }).then(resizeResult => {
        return this.successFn(resizeResult)
    });
}

Hide Image Element

If you notice when we first time load the component (in the new stage). The user will encounter an image-broken icon. To avoid this, we need to hide the image-element if there is no valid src set.

Image-broken icon if no picture uploaded

image.spec.ts:

it('set visible image', () => {
    const img = new image(context, htmlDivElement);
    img.generate();
    const imgCtrl = document.getElementById(controls.image) as HTMLImageElement;
    imgCtrl.src = '#';

    img.setVisible();
    const imgClass = imgCtrl.getAttribute('class') || '';
    expect(imgClass.indexOf('d-none') > 0).to.be.true;
});

image.ts:

   setVisible() {
    const image = document.getElementById(controls.image) as HTMLImageElement;
    const imageClass = [
        'img-fluid',
        (image.src.indexOf('data:') > -1 ? '' : 'd-none')
    ].join(' ').trim();

    image.setAttribute('class', imageClass);
}

Implementation to call setVisible method:

generate() {
    const maindiv = document.createElement('div') as HTMLDivElement;
    maindiv.setAttribute('class', 'mb-3');
    maindiv.setAttribute('id', controls.div);

    maindiv.appendChild(this.getImage());

    this.htmlDivElement.appendChild(maindiv);
    this.setVisible();
}
...
setSrc(imageString: string | ArrayBuffer) {
    const image = document.getElementById(controls.image) as HTMLImageElement;
    image.setAttribute('src', imageString as string);

    this.setVisible();
}

Image Resizer

For the image-resizer feature, we will use the Html5 Canvas method (which is easier to implement). We just need to load the image to the canvas and in canvas, there is a method called toDataUrl with 2 parameters. The first param is for the file-type, and the second one is the quality of the file. On this sample, we will set the quality to 60% (0.6).

file-upload.spec.ts:

it('can reduce the size of the image', async () => {
    const base64Image = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7';
    const file = new fileUpload(context, htmlDivElement, () => { });
    const resize = await file.resize(base64Image);
    expect(resize).to.not.null;
});

file-upload.ts:

resize(base64String: string | ArrayBuffer): Promise<string> {
    return new Promise((resolve, reject) => {
      const img = document.createElement('img');
      const text = base64String.toString();
      const filetype = text.split(';')[0].replace('data:', '');

      const resizeFactor = 0.6;
      img.onload = function () {
        const canvas = document.createElement('canvas');
        canvas.height = img.height;
        canvas.width = img.width;
        const ctx = canvas.getContext('2d') as CanvasRenderingContext2D;
        ctx.drawImage(img, 0, 0);
        debugger;

        const result = canvas.toDataURL(filetype, resizeFactor);
        resolve(result);
      }

      img.onerror = function () {
        reject(new DOMException("Problem resizing file image."));
      }

      img.src = text;
    });
}

Implementation:

submit(): void {
    const imageFile = document.getElementById(
      controls.file
    ) as HTMLInputElement;
    const information = document.getElementById(controls.information);
    if (!information) return;

    const filePath = imageFile.value;
    information.style.display = !filePath ? 'block' : 'none';

    if (!filePath) {
      return;
    }

    const file = imageFile.files![0];
    this.convertToBase64(file as Blob).then(base64String => {
      this.clearFile();
      return this.resize(base64String);
    }).then(resizeResult => {
      return this.successFn(resizeResult)
    });
}

Here is the demonstration after we pack the component and install it in our CRM Environment:

Demonstration
Resize Result (index.jpg) more smaller compare to the input image (sample.jpg)

Summary

  • Sometimes we need to make alternative ways to make the code ‘testable’. As far as my understanding, we are supposed not to compromise (put testing code in the implementation code). The clearFile method is an example of Technical Debt that we need to pay in the future. Is not a good solution, but is the best solution with my current knowledge. But rely on unit testing is a good practice to do because it will help developers, managers, and the organization in particular to prevent regression. This is the main benefit of Test-Driven-Development in general.
  • When doing it this way, I just need to repackage and redeploy it 3 times (1 for first commit, 1 manual test that proved I need to set imageFile.value = ”. And the last one is for the fixes). Which in my opinion is far more effective compare to manually testing it (and you should try it too).

You can refer the full code on this blog post series in here.

Creating Better PCF Component Series:

3 thoughts on “Creating Better PCF Component – Part 3

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.