Binding issue between custom element and its parent

Hello, I’d like to start by saying that I am completely noob to Aurelia.

I am trying to create a custom Mapbox class that would render mapbox map and geocoder. I’d like this custom element to take in initial values from wherever it is initialized and whenever geocoder is used it should bind the response of the geocoder back to whever it is bound.

Dropping the whole code here:

@autoinject()
@customElement("map")
export class Mapbox {
    @bindable initialCenterLng: number = 0;
    @bindable initialCenterLat: number = 0;
    @bindable initialZoom: number = 0;
    @bindable geocodeLatResponse: number;
    @bindable geocodeLngResponse: number;
    @bindable geocodeAddressResponse: string;

    private map: mapboxgl.Map;
    private mapContainer: HTMLElement;
    private geocoder: MapboxGeocoder;
    private geocoderContainer: HTMLElement;
    private markers: Array<mapboxgl.Marker> = [];
    private token: string = ""

    constructor(
        private element: Element,
    ) {
    }

    attached() {
        this.initializeMap();
        this.initializeGeocoder();
    }

    detached() {
        this.map.remove();
        this.geocoderContainer.remove();
    }

    private initializeMap() {
        this.mapContainer = this.element.querySelector(".map-container");
        this.map = new mapboxgl.Map({
            accessToken: this.token,
            container: this.mapContainer,
            style: "mapbox://styles/mapbox/streets-v11",
            center: [this.initialCenterLng, this.initialCenterLat],
            zoom: this.initialZoom,
        });
        this.map.on("load", () => {
            this.map.resize();
            this.geocoderContainer.appendChild(this.geocoder.onAdd(this.map));
        });
    }

    private initializeGeocoder() {
        this.geocoderContainer = this.element.querySelector(".geocoder");
        this.geocoder = new MapboxGeocoder({
            accessToken: this.token,
            mapboxgl,
            placeholder: "Search address",
        });
        this.geocoder.on("result", async (event: any) => {
            this.addMarker([this.initialCenterLng, this.initialCenterLat], true);
            const {center, place_name} = event.result;
            this.geocodeLatResponse = center[1];
            this.geocodeLngResponse = center[0];
            this.geocodeAddressResponse = place_name;
            this.addMarker(center);
            if (this.markers.length === 2) {
                const response = await fetch(`https://api.mapbox.com/directions/v5/mapbox/driving/
                ${(this.initialCenterLng)},${(this.initialCenterLat)};${this.geocodeLngResponse},${this.geocodeLatResponse}
                ?geometries=geojson&access_token=${this.token}&steps=true`);
                const json = await response.json();

                if (json.routes.length) {
                    this.drawLine(json.routes[0].geometry.coordinates);
                }
            }
        });
        this.geocoder.on("clear", () => {
            this.markers.forEach((marker) => marker.remove());
            this.markers = [];
            if (this.map.getLayer("line")) {
                this.map.removeLayer("line");
            }
        });
    }

    private addMarker(coords: [number, number], isStartMarker: boolean = false) {
        const iconUrl = isStartMarker ? "../../images/mapbox/start.png" : "../../images/mapbox/finish.png";
        const marker = new mapboxgl.Marker({
            element: this.createMarkerElement(iconUrl, 16),
        }).setLngLat(coords).addTo(this.map);
        this.markers.push(marker);
    }

    private createMarkerElement(iconUrl: string, iconSize: number): HTMLElement {
        const markerEl = document.createElement("div");
        markerEl.style.backgroundImage = `url(${iconUrl})`;
        markerEl.style.backgroundSize = "contain";
        markerEl.style.width = `${iconSize}px`;
        markerEl.style.height = `${iconSize}px`;
        return markerEl;
    }

    private drawLine(coordinates: any) {
        const geojson = {
            type: "Feature",
            properties: {},
            geometry: {
                type: "LineString",
                coordinates,
            },
        };

        if (this.map.getSource("line")) {
            this.map.getSource("line").setData(geojson);
        } else {
            this.map.addSource("line", {
                type: "geojson",
                data: geojson,
            });

            this.map.addLayer({
                id: "line",
                type: "line",
                source: "line",
                layout: {
                    "line-join": "round",
                    "line-cap": "round",
                },
                paint: {
                    "line-color": "#000",
                    "line-width": 5,
                    "line-opacity": 1,
                    "line-dasharray": [1, 1],
                },
            });
        }
    }
}

This code works fine if I define the initial values inside the custom element but it won’t get values through bind. Anyways, here is a snippet from my parent component and its html where i import custom-element.

@autoinject()
@transient()
export class SomeOtherClass { 

    @bindable initialCenterLng: number = 32.866287;
    @bindable initialCenterLat: number = 39.925533;
    @bindable initialZoom: number = 12;
    @bindable geocodeLatResponse: number;
    @bindable geocodeLngResponse: number;
    @bindable geocodeAddressResponse: string;
     // Some other code
}

SomeOtherClass.html

<require from="../../../custom-elements/map/map"></require>
<map initialCenterLng.bind="initialCenterLng" initialCenterLat.bind="initialCenterLat" initialZoom.bind="initialZoom" geocodeLatResponse.bind="geocodeLatResponse" geocodeLngResponse.bind="geocodeLngResponse" geocodeAddressResponse.bind="geocodeAddressResponse"></map>

With this setup, custom element neither binding value from parent nor returns any value to it. What am I missing? I am gonna lose it soon…

Thanks,

I managed to pass the initial values to child component by changing

<map initial-center-lng.bind="initialCenterLng" initial-center-lat.bind="initialCenterLat" initial-zoom.bind="initialZoom" geocodeLatResponse.bind="geocodeLatResponse" geocodeLngResponse.bind="geocodeLngResponse" geocodeAddressResponse.bind="geocodeAddressResponse"></map>

But now i am having troubles binding the value from child to parent… the kebab case conversion did not work for that

Use .two-way for that, instead of .bind (docs).

1 Like