Load styles in Ionic dynamically from a file downloaded via http

First of all, sorry for my English…

I’m trying to develop an Ionic app with Capacitor using standalone Angular.

I would like it to have pre-established styles by default and for them to be declared in my variables file within the theme folder.

At the beginning of loading the app I check whether or not it is configured to redirect to the configuration page in the ‘/config’ path to collect some user values ​​and download its configuration file:

private async ionViewDidEnter(): Promise<void> {
    Promise.all([
        this._services._storage.getItem('configClient', true),
        this._services._storage.getItem('news', true),
        this._services._storage.getItem('chg_pin', true),
        this._services._storage.getItem('user', true),
        this._services._storage.getItem('remember', true),
    ])
    .then(async (responses) => {
        // Show configuration page or not if it is the first time you connect.
        if(responses[0] == null) {
            this._services._router.navigate(['/config']);
        }else {
            // Loading client settings and styles if you have ever connected
            await this._services._configuration.getStorageClientConfig();
      
        }  
    })
    .catch((e) => {
        console.error('ErrorInitLogin', e.message);
    })
    .finally(() => {
        this._services.splashscreen.hide();
    });
}

If you do not have previous configuration:

public async sendConfig(configForm): Promise<void> {
    if(this.configForm.invalid) return;
    await this._services._loading.showLoading(this._services._translate.instant('pages.configuration.sending_data'), 0);
    this._services._configuration.initClientConfig()
    .then(() => {
        this._services._loading.dismiss();
        this._services._menu.swipeGesture(true, 'menu-ctx');
        this._services._router.navigate(['login']); 
    })
    .catch(() => {
        this._services._loading.dismiss();
        this._services._toast.showToast(this._services._translate.instant('pages.configuration.error_sending_data'), 'bottom', 'danger');
    });
}

Then I call a configuration service that downloads a configuration file for that user from an ftp server.

public initClientConfig(): Promise<boolean> {
    return new Promise((resolve, reject) => {
        // Recover Settings from Server
        this.getConfig()
        .then((response) => {
            // Set Servers
            this.setAppServer();
            // Initialize Modules
            this.setAppModules();
            resolve(true);
        });
    });
}

The service downloads the file as follows:

public getConfig(): Promise<ConfiguracionCliente> {
    return new Promise((resolve) => {
        this._http.send(
            this.currentClient.url, 
            'GET', 
            {}, 
            [
                {
                    "x-auth-header": "**********************************"
                }
            ]
        )
        .then(async (response: ConfiguracionCliente) => {
            this.clientConfig = response;
            if(this.clientConfig && !GenericMethods.isNullOrEmpty(this.clientConfig.build)) {
               this._storage.setItem('configClient', this.clientConfig, true);
               this._theme.initClientTheme(this.clientConfig);
                if(this.clientConfig.servers?.length > 0) {
                    this._server.servers = this.clientConfig.servers;
                }
                if(this.clientConfig.modulos?.length > 0) {
                    this._modules.arrayModules = this.clientConfig.modulos;
                }
                if(!GenericMethods.isNullOrEmpty(this._app.appInfo.version) && (this._app.appInfo.version < this.clientConfig.version)) {
                    this.showPermissions = true;
                    const version = await this._modal.create({
                        component: ModalVersion,
                        id: 'version',
                        initialBreakpoint: 1,
                        breakpoints: [0, 1],
                        backdropDismiss: false
                    });
                    await version.present();
                }
            }
            resolve(this.clientConfig);
        })
        .catch((e) => {
            console.error('ErrorGetClientConfig: ', e.message);
            this.getStorageClientConfig()
            .then((response) => {
                if(response) {
                    resolve(this.clientConfig);
                }else {
                    resolve(null);
                }
            })
        });
    })
}

From that file I get the name of the client, its logo, servers, modules and a property with the styles as follows

{
"version": "3.0.0",
"build": 81,
"usuario": "NombreUsuario",
"logo": "https://url_que_apunta_al_logo",
"servers": [
    {...},
    {...}
],
"modules": [
    {...},
    {...}
],
"theme": {
    "primary": "#FF6600 !important",
    "primary-rgb": "255,102,0 !important",
    "primary-contrast": "#ffffff !important",
    "primary-contrast-rgb": "0,0,0 !important",
    "primary-shade": "#e05a00 !important",
    "primary-tint": "#ff751a !important",

    "secondary": "#FF8D00 !important",
    "secondary-rgb": "255,141,0 !important",
    "secondary-contrast": "#ffffff !important",
    "secondary-contrast-rgb": "0,0,0 !important",
    "secondary-shade": "#e07c00 !important",
    "secondary-tint": "#ff981a !important",

    "tertiary": "#FFC299 !important",
    "tertiary-rgb": "255,194,153 !important",
    "tertiary-contrast": "#ffffff !important",
    "tertiary-contrast-rgb": "0,0,0 !important",
    "tertiary-shade": "#e0ab87 !important",
    "tertiary-tint": "#ffc8a3 !important",

    "success": "#439467 !important",
    "success-rgb": "67,148,103 !important",
    "success-contrast": "#ffffff !important",
    "success-contrast-rgb": "255,255,255 !important",
    "success-shade": "#3b825b !important",
    "success-tint": "#569f76 !important",

    "warning": "#FF8D00 !important",
    "warning-rgb": "255,141,0 !important",
    "warning-contrast": "#000000 !important",
    "warning-contrast-rgb": "0,0,0 !important",
    "warning-shade": "#e07c00 !important",
    "warning-tint": "#ff981a !important",

    "danger": "#CE352C !important",
    "danger-rgb": "206,53,44 !important",
    "danger-contrast": "#ffffff !important",
    "danger-contrast-rgb": "255,255,255 !important",
    "danger-shade": "#b52f27 !important",
    "danger-tint": "#d34941 !important",

    "dark": "#222222 !important",
    "dark-rgb": "34,34,34 !important",
    "dark-contrast": "#ffffff !important",
    "dark-contrast-rgb": "255,255,255 !important",
    "dark-shade": "#1e1e1e !important",
    "dark-tint": "#383838 !important",

    "medium": "#999999 !important",
    "medium-rgb": "153,153,153 !important",
    "medium-contrast": "#000000 !important",
    "medium-contrast-rgb": "0,0,0 !important",
    "medium-shade": "#878787 !important",
    "medium-tint": "#a3a3a3 !important",

    "light": "#f7f7f7 !important",
    "light-rgb": "247,247,247 !important",
    "light-contrast": "#000000 !important",
    "light-contrast-rgb": "0,0,0 !important",
    "light-shade": "#d9d9d9 !important",
    "light-tint": "#f8f8f8 !important",

    "header": "#e05a00 !important",
    "header-rgb": "224,90,0 !important",
    "header-contrast": "#ffffff !important",
    "header-contrast-rgb": "255,255,255 !important",
    "header-shade": "#c54f00 !important",
    "header-tint": "#e36b1a !important"
    }
}

The configuration service, once the file is downloaded, calls the theme service to start the theme configuration with the values ​​brought from the downloaded file as follows:

public initClientTheme(clientConfig: ConfiguracionCliente): Promise<boolean> {
    return new Promise((resolve) => {
        if(clientConfig && clientConfig.theme) {
            this.nameEmpresa = clientConfig.empresa;
            this.logo = clientConfig.logo;
            this.temaCliente = clientConfig.theme;
            this.ngZone.run(() => {
                Object.keys(this.temaCliente).forEach((key) => {
                    this.render.setStyle(document.documentElement, `--ion-color-${key}`, this.temaCliente[key]);
                });
            });
            resolve(true);
        }else {
            console.error('ErrorInitClientTheme');
            resolve(false);
        }
        console.log(`--ion-color-primary: ${JSON.stringify(document.documentElement.style)}`);
    });
}

The fact is that in that last ‘console.log(–ion-color-primary: ${JSON.stringify(document.documentElement.style)})’ it prints the values ​​of the :root with the variables and values ​​brought from the configuration file, but it is not rendered with the new colors, but with the previous ones…

Any ideas on how to dynamically change the styles of the application based on the downloaded file and how to make it persistent so that those styles are loaded whenever the app is reopened once configured to start?

Thanks guys!!!

I would like to know if any of you can think of how I can download that configuration file and with it configure my application based on the logged in user, placing their styles and making them persistent whenever the application is reopened.

Someone know any guide? Thanks in advance!!

I just did this in our Ionic Vue app. I think your problem is that you are doing setStyle instead of setProperty.

Here is what we are doing:

private configureColors(group: GroupModel): void {
    const rootElm = document.querySelector(':root') as HTMLElement

    group.colors.forEach(color => rootElm.style.setProperty(color.name, color.value))
}

private removeColors(group: GroupModel): void {
    const rootElm = document.querySelector(':root') as HTMLElement

    group.colors.forEach(color => rootElm.style.removeProperty(color.name))
}

With our colors in this format:

{
    "colors": [
        {
            "name": "--ion-background-color",
            "value": "#f7fdff"
        },
        {
            "name": "--ion-background-color-rgb",
            "value": "247, 253, 255"
        },
        {
            "name": "--ion-color-primary",
            "value": "#2679ac"
        },
        {
            "name": "--ion-color-primary-rgb",
            "value": "38, 121, 172"
        },
        {
            "name": "--ion-color-primary-shade",
            "value": "#0e4567"
        }
    ]
}

We save out the custom colors in local storage and then set them every time the app launches.

In regards to the logo, we download it as a blob and save that out in local storage as a Blob itself. We then display it using URL.createObjectURL. We went with this over saving the logo/image to the file system as it’s simpler and doesn’t require a native plugin to interact with the file system.

Here is the full code. I know it is Vue, but you should give you some idea on how to implement.

<template>
    <div v-if="hasLogo && blobUrl != ''" class="mb-4 flex justify-center">
        <img
            class="max-h-16"
            :src="blobUrl"
            :alt="`${primaryGroup?.name} logo`"
            @load="handleLoaded"
        />
    </div>
</template>

<script setup lang="ts">
import { useGroupManager } from '@/groups/group-manager'
import { useGroupStore } from '@/stores/group-store'
import { computed, ref, watch } from 'vue'

const groupStore = useGroupStore()

const blobUrl = ref('')

const primaryGroup = computed(() => groupStore.groups.primary())
const hasLogo = computed(() => primaryGroup.value?.logo.url != null)

watch(
    () => primaryGroup.value?._is_downloading_logo,
    async isDownloading => {
        if (!isDownloading) {
            getLogo()
        }
    },
    { immediate: true },
)

async function getLogo(): Promise<void> {
    const data = await useGroupManager().getLogo() // A helper method to get blob from local storage

    if (data == null) {
        return
    }

    blobUrl.value = URL.createObjectURL(data)
}

function handleLoaded(): void {
    if (blobUrl.value === '') {
        return
    }
    
    // Clearing this doesn't cause the image to disappear. It stays rendered.
    URL.revokeObjectURL(blobUrl.value)
}
</script>
1 Like

Thanks a ton!!

This really worked on my project!

I’ve been thinking about this issue for two days… and I didn’t really know how to approach the problem, and now that I have seen the solution, it makes me angry that I haven’t seen it before!

I really appreciate you taking the time to guide me through this.

Here you have a friend forever!

:hugs:​:fist_right:t3:​:fist_left:t3:

1 Like