🛠️ Tiny: Passing Props

Hey ,

I'm thrilled to help you learn JavaScript. Unfortunately, you've landed on a page where you cannot access with your current purchase.

Please upgrade (use this link) access this content.

I'm super eager to help you learn more!

🛠️ Tiny: Passing Props

To make the Total Count component, we need to get the count from both parent and child components. One way to do this is to pass properties down into Total Count. (We call this props for short).

Here’s what Total Count looks like at this point:

// total-count.js
export default Tiny({
  template () {
    return `
      <div class="component total-component flow text">
        <h2>Total Count</h2>
        <ul>
          <li>Parent Count: ${this.state.count}</li>
          <li>Child Count: ${this.state.childCount}</li>
          <li>Total Count: ${this.state.count + this.state.childCount}</li>
        </ul>
      </div>
    `
  }
})

We’ll start Total Count by importing it into the parent component.

// main.js
import totalCount from './total-count.js'

Tiny({
  // ...
  components: {
    child,
    totalCount
  },
  // ...
  template () {
    return `
      <div class="component parent-component flow">
        <!-- ... -->
        <div class="half">
          <div tiny-component="child"></div>
          <div tiny-component="totalCount"></div>
        </div>
      </div>
    `
  }
})

When you do this you’ll encounter an error that says Cannot read property 'count' of undefined.

This error occurs because Total Count doesn’t have count and childCount declared in its state. We’ll remove these two properties for now since we’re going to pass props down from the parent component.

Passing props down from parents

We can pass props down with a tiny-props custom attribute. Each prop will contain a property name and a value like this:

Tiny({
  // ...
  template () {
    return `
      <div
        class="component parent-component flow"
        ...
      >
        <div class="half">
          <!-- ... -->
          <div tiny-component="totalCount" tiny-props="[parentCount, 10]"></div>
        </div>
      </div>
    `
	}
})

We want to be able to use the parentCount prop in Total Count like this:

// total-count.js
export default Tiny({
  template () {
    return `
      <div class="component total-component flow text">
        <h2>Total Count</h2>
        <ul>
          <li>Parent Count: ${this.props.parentCount}</li>
        </ul>
      </div>
    `
  }
})

To do this, we need to add tiny-props values into the component before we render it.

export default function Tiny (options) {
  // ...
  function _renderChildComponents (options) {
    // ...
    for (const compEl of compEls) {
      // ...
      // Add props here
      _render(comp)
      _addEventListeners(comp)
    }
  }
}

To add props, we need to get the prop string with getAttribute

export default function Tiny (options) {
  // ...
  function _renderChildComponents (options) {
    // ...
    for (const compEl of compEls) {
      // ...
      const attribute = compEl.getAttribute('tiny-props')
    }
    // ...
  }
}

If the element contains props, we want to extract these props into a property-value pair. We can use the same code we wrote for the event listeners here.

export default function Tiny (options) {
  // ...
  function _renderChildComponents (options) {
    // ...
    for (const compEl of compEls) {
      // ...
      const attribute = compEl.getAttribute('tiny-props')
      if (attribute) {
        const props = attribute
          .replace('[', '')
          .replace(']', '')
          .split(',')
          .map(l => l.trim())
        console.log(props)
      }
    }
    // ...
  }
}

As I mentioned before, the downside to passing values via custom attributes is everything gets converted into strings. So we can’t simply accept values via the tiny-props property. We need a way to transfer other types into the child component. We’ll work on this later.

At this point, we can set the property-value pair into a props object.

export default function Tiny (options) {
  // ...
  function _renderChildComponents (options) {
    // ...
    for (const compEl of compEls) {
      // ...
      const attribute = compEl.getAttribute('tiny-props')
      if (attribute) {
        // ...
        comp.props[props[0]] = props[1]
      }
    }
    // ...
  }
}

You’ll see an error that says Cannot set property 'parentCount' of undefined.

This error happens because Tiny doesn’t contain a props object yet. We can fix this by passing a props object when we initialize Tiny.

const initial = {
  // ...
  props: {}
}

export default function Tiny (options) {
  options = Object.assign({}, initial, options)
  // ...
}

Passing other kinds of values

We can pass other values by writing the key to a property in tiny-props. For example, we can pass the entire state object by writing state

Tiny({
  // ...
  template () {
    return `
      <div ... >
        // ...
        <div class="half">
          <div tiny-component="child"></div>
          <div tiny-component="totalCount" tiny-props="[parentState, state]"></div>
        </div>
      </div>
    `
  }
})

In Tiny, we can check whether the passed value is a property in the component. If that’s the case, we send the property’s value into the child component. Otherwise, we simply send it as a hard-coded string.

export default function Tiny (options) {
  // ...
  function _renderChildComponents (options) {
    // ...
    for (const compEl of compEls) {
      // ...
      const attribute = compEl.getAttribute('tiny-props')
      if (attribute) {
        // ...
        const prop = props[0]
        const value = props[1]
        if (options[value]) {
          comp.props[prop] = options[value]
        } else {
          comp.props[prop] = value
        }
      }
    }
    // ...
  }
}

We can now get the parent’s count like this:

// total-count.js
export default Tiny({
  template () {
    return `
      <div class="component total-component flow text">
        <h2>Total Count</h2>
        <ul>
          <li>Parent Count: ${this.props.parentState.count}</li>
        </ul>
      </div>
    `
  }
})

The count will be updated when the parent’s state changes.

Passing state values

Children components don’t need the entire parent’s state most of the time, so we want to pass values that matters.

In this case, we want to pass state.count and not the entire state object.

Tiny({
  // ...
  template () {
    return `
      <div ... >
        <!-- ... -->
        <div class="half">
          <!-- ... -->
          <div tiny-component="totalCount" tiny-props="[parentCount, state.count]"></div>
        </div>
      </div>
    `
  }
})

If we tried getting parentCount in totalCount now, we’ll get the string state.count.

This happens because we used the string state.count when searching for a property in the parent component. Since options['state.count']doesn’t exist as a property, the output value becomes a string.

We need to use options['state']['count'] instead of options['state.count'] to extract the actual value.

To do this, we need to split state.count with .. Then, we reduce each item in the array into the eventual property.

export default function Tiny (options) {
  // ...
  function _renderChildComponents (options) {
    // ...
    for (const compEl of compEls) {
      // ...
      const attribute = compEl.getAttribute('tiny-props')
      if (attribute) {
        // ...
        const prop = props[0]
        const value = props[1]
          .split('.')
          .reduce((acc, current) => acc[current], options)
        console.log(value)
      }
    }
    // ...
  }
}

In this case, we get the value, which is a number.

So we need to check against three cases now:

  1. The user passes in a hardcoded value, which is always converted into a string.
  2. The user passes in a value that matches the component’s property.
  3. The user passes in a nested value like state.count.

This three-way flow makes adding props a bit more complex, so we should put the entire segment into its own function. We’ll call this function _addProps.

function _addProps (comp) {
  // ...
}

We make a call to this function in _renderChildComponents.

function _renderChildComponents (options) {
      // ...
      _addProps(comp)
      _render(comp)
      _addEventListeners(comp)
    }
  }

We’ll copy-paste everything we wrote into this function.

export default function Tiny (options) {
  // ...
  function _addProps () {
    // Add props
    const attribute = compEl.getAttribute('tiny-props')
    if (attribute) {
      const props = attribute
        .replace('[', '')
        .replace(']', '')
        .split(',')
        .map(l => l.trim())

      const prop = props[0]
      const value = props[1]
        .split('.')
        .reduce((acc, current) => acc[current], options)

      if (options[value]) {
        comp.props[prop] = options[value]
      } else {
        comp.props[prop] = value
      }
    }
  }
  // ...
}

We will pass in comp into this function and use comp.element instead of compEl.

export default function Tiny (options) {
  // ...
  function _addProps (comp) {
    // Add props
    const attribute = comp.element.getAttribute('tiny-props')
    // ...
  }
  // ...
}

We don’t need to add props if the element doesn’t contain a tiny-props attribute, so we can use an early return.

export default function Tiny (options) {
  // ...
  function _addProps (comp) {
    // Add props
    const attribute = comp.element.getAttribute('tiny-props')
    if (!attribute) return
    // ...
  }
  // ...
}

We’re now ready to tackle the three scenarios. Again, they are:

  1. The user passes in a hardcoded value, which is always converted into a string.
  2. The user passes in a value that matches the component’s property.
  3. The user passes in a nested value like state.count.

We can check for a . for the third case, so let’s begin there.

export default function Tiny (options) {
  // ...
  function _addProps (comp) {
    // ...
    const prop = props[0]
    let value = props[1]

    if (value.includes('.')) {
      // Third case
      return
    }

    // First and second case
  }
  // ...
}

For the third case, we need to perform split and reduce then assign the eventual value as props.

export default function Tiny (options) {
  // ...
  function _addProps (comp) {
    // ...
    const prop = props[0]
    let value = props[1]

    if (value.includes('.')) {
      value = value
        .split('.')
        .reduce((acc, current) => acc[current], options)
      comp.props[prop] = value
      return
    }

    // First and second cases
  }
  // ...
}

For the second case, we can check whether options[value] is valid. If it is, we’ll pass that value as props.

export default function Tiny (options) {
  // ...
  function _addProps (comp) {
    // ...
    if (options[value]) {
      comp.props[prop] = options[value]
      return
    }
  }
  // ...
}

For the first case, we’ll simply pass the value as props.

export default function Tiny (options) {
  // ...
  function _addProps (comp) {
    // ...
    comp.props[prop] = value
  }
  // ...
}

The component should work with all three types of props now.